first commit
This commit is contained in:
907
remotecontrol.py
Normal file
907
remotecontrol.py
Normal file
@@ -0,0 +1,907 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
LLM_Train Remote Control
|
||||
|
||||
A standalone GUI application for remotely controlling the LLM_Train MCP server.
|
||||
Allows users to connect to the server, load models, configure settings, and
|
||||
perform inference operations remotely.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox, scrolledtext, filedialog
|
||||
from typing import Dict, Any, List, Optional, Callable
|
||||
import subprocess
|
||||
import os
|
||||
from pathlib import Path
|
||||
import queue
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class MCPClient:
|
||||
"""Client for connecting to MCP server via subprocess."""
|
||||
|
||||
def __init__(self):
|
||||
self.process = None
|
||||
self.connected = False
|
||||
self.request_id = 0
|
||||
self.callbacks: Dict[str, List[Callable]] = {
|
||||
'on_connect': [],
|
||||
'on_disconnect': [],
|
||||
'on_error': [],
|
||||
'on_response': []
|
||||
}
|
||||
self.pending_requests: Dict[int, Callable] = {}
|
||||
self.reader_thread = None
|
||||
self.writer_queue = queue.Queue()
|
||||
self.writer_thread = None
|
||||
|
||||
def register_callback(self, event: str, callback: Callable):
|
||||
"""Register a callback for client events."""
|
||||
if event in self.callbacks:
|
||||
self.callbacks[event].append(callback)
|
||||
|
||||
def _trigger_callback(self, event: str, *args, **kwargs):
|
||||
"""Trigger callbacks for an event."""
|
||||
for callback in self.callbacks.get(event, []):
|
||||
try:
|
||||
callback(*args, **kwargs)
|
||||
except Exception as e:
|
||||
print(f"Callback error: {e}")
|
||||
|
||||
async def connect(self, server_path: str = "mcp_server.py"):
|
||||
"""Connect to the MCP server."""
|
||||
try:
|
||||
# Start the MCP server process
|
||||
self.process = subprocess.Popen(
|
||||
[sys.executable, server_path],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
bufsize=0
|
||||
)
|
||||
|
||||
self.connected = True
|
||||
|
||||
# Start reader and writer threads
|
||||
self.reader_thread = threading.Thread(target=self._reader_loop, daemon=True)
|
||||
self.writer_thread = threading.Thread(target=self._writer_loop, daemon=True)
|
||||
|
||||
self.reader_thread.start()
|
||||
self.writer_thread.start()
|
||||
|
||||
# Send initialization request
|
||||
init_request = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "initialize",
|
||||
"params": {
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {
|
||||
"roots": {
|
||||
"listChanged": True
|
||||
},
|
||||
"sampling": {}
|
||||
},
|
||||
"clientInfo": {
|
||||
"name": "LLM_Train Remote Control",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
},
|
||||
"id": self._get_request_id()
|
||||
}
|
||||
|
||||
await self._send_request(init_request)
|
||||
self._trigger_callback('on_connect')
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self._trigger_callback('on_error', f"Connection failed: {e}")
|
||||
return False
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect from the MCP server."""
|
||||
self.connected = False
|
||||
|
||||
if self.process:
|
||||
try:
|
||||
self.process.terminate()
|
||||
self.process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.process.kill()
|
||||
except Exception:
|
||||
pass
|
||||
self.process = None
|
||||
|
||||
self._trigger_callback('on_disconnect')
|
||||
|
||||
def _get_request_id(self) -> int:
|
||||
"""Get next request ID."""
|
||||
self.request_id += 1
|
||||
return self.request_id
|
||||
|
||||
async def _send_request(self, request: Dict[str, Any], callback: Optional[Callable] = None):
|
||||
"""Send a request to the server."""
|
||||
if not self.connected or not self.process:
|
||||
return
|
||||
|
||||
request_id = request.get('id')
|
||||
if request_id and callback:
|
||||
self.pending_requests[request_id] = callback
|
||||
|
||||
# Queue the request for the writer thread
|
||||
self.writer_queue.put(json.dumps(request) + '\n')
|
||||
|
||||
def _writer_loop(self):
|
||||
"""Writer thread loop."""
|
||||
while self.connected and self.process:
|
||||
try:
|
||||
# Get request from queue
|
||||
request_str = self.writer_queue.get(timeout=1)
|
||||
|
||||
if self.process and self.process.stdin:
|
||||
self.process.stdin.write(request_str)
|
||||
self.process.stdin.flush()
|
||||
|
||||
except queue.Empty:
|
||||
continue
|
||||
except Exception as e:
|
||||
if self.connected:
|
||||
self._trigger_callback('on_error', f"Write error: {e}")
|
||||
break
|
||||
|
||||
def _reader_loop(self):
|
||||
"""Reader thread loop."""
|
||||
while self.connected and self.process:
|
||||
try:
|
||||
if self.process and self.process.stdout:
|
||||
line = self.process.stdout.readline()
|
||||
if not line:
|
||||
break
|
||||
|
||||
line = line.strip()
|
||||
if line:
|
||||
try:
|
||||
response = json.loads(line)
|
||||
self._handle_response(response)
|
||||
except json.JSONDecodeError as e:
|
||||
self._trigger_callback('on_error', f"JSON decode error: {e}")
|
||||
|
||||
except Exception as e:
|
||||
if self.connected:
|
||||
self._trigger_callback('on_error', f"Read error: {e}")
|
||||
break
|
||||
|
||||
# Connection lost
|
||||
if self.connected:
|
||||
self.connected = False
|
||||
self._trigger_callback('on_disconnect')
|
||||
|
||||
def _handle_response(self, response: Dict[str, Any]):
|
||||
"""Handle response from server."""
|
||||
request_id = response.get('id')
|
||||
|
||||
if request_id and request_id in self.pending_requests:
|
||||
callback = self.pending_requests.pop(request_id)
|
||||
callback(response)
|
||||
|
||||
self._trigger_callback('on_response', response)
|
||||
|
||||
async def list_tools(self, callback: Optional[Callable] = None):
|
||||
"""List available tools on the server."""
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "tools/list",
|
||||
"id": self._get_request_id()
|
||||
}
|
||||
await self._send_request(request, callback)
|
||||
|
||||
async def call_tool(self, tool_name: str, arguments: Dict[str, Any], callback: Optional[Callable] = None):
|
||||
"""Call a tool on the server."""
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": tool_name,
|
||||
"arguments": arguments
|
||||
},
|
||||
"id": self._get_request_id()
|
||||
}
|
||||
await self._send_request(request, callback)
|
||||
|
||||
|
||||
class RemoteControlGUI:
|
||||
"""Main GUI for the remote control application."""
|
||||
|
||||
def __init__(self):
|
||||
self.root = tk.Tk()
|
||||
self.root.title("LLM_Train Remote Control")
|
||||
self.root.geometry("1000x700")
|
||||
self.root.minsize(800, 600)
|
||||
|
||||
# Initialize MCP client
|
||||
self.client = MCPClient()
|
||||
self.client.register_callback('on_connect', self._on_connect)
|
||||
self.client.register_callback('on_disconnect', self._on_disconnect)
|
||||
self.client.register_callback('on_error', self._on_error)
|
||||
|
||||
# UI state
|
||||
self.connected = False
|
||||
self.available_tools = []
|
||||
self.available_models = []
|
||||
self.current_model = None
|
||||
self.system_info = {}
|
||||
|
||||
# Setup UI
|
||||
self._setup_ui()
|
||||
|
||||
# Setup asyncio loop for MCP client
|
||||
self.loop = asyncio.new_event_loop()
|
||||
self.loop_thread = threading.Thread(target=self._run_event_loop, daemon=True)
|
||||
self.loop_thread.start()
|
||||
|
||||
def _run_event_loop(self):
|
||||
"""Run asyncio event loop in separate thread."""
|
||||
asyncio.set_event_loop(self.loop)
|
||||
self.loop.run_forever()
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Setup the main UI."""
|
||||
# Menu bar
|
||||
self._create_menu()
|
||||
|
||||
# Main container
|
||||
main_frame = ttk.Frame(self.root)
|
||||
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
# Connection frame
|
||||
self._create_connection_frame(main_frame)
|
||||
|
||||
# Notebook for tabs
|
||||
self.notebook = ttk.Notebook(main_frame)
|
||||
self.notebook.pack(fill=tk.BOTH, expand=True, pady=(10, 0))
|
||||
|
||||
# Model Management tab
|
||||
self._create_model_tab()
|
||||
|
||||
# Inference tab
|
||||
self._create_inference_tab()
|
||||
|
||||
# System Info tab
|
||||
self._create_system_tab()
|
||||
|
||||
# Log tab
|
||||
self._create_log_tab()
|
||||
|
||||
# Status bar
|
||||
self._create_status_bar(main_frame)
|
||||
|
||||
def _create_menu(self):
|
||||
"""Create menu bar."""
|
||||
menubar = tk.Menu(self.root)
|
||||
self.root.config(menu=menubar)
|
||||
|
||||
# File menu
|
||||
file_menu = tk.Menu(menubar, tearoff=0)
|
||||
menubar.add_cascade(label="File", menu=file_menu)
|
||||
file_menu.add_command(label="Connect to Server", command=self._connect_dialog)
|
||||
file_menu.add_separator()
|
||||
file_menu.add_command(label="Exit", command=self.root.quit)
|
||||
|
||||
# Tools menu
|
||||
tools_menu = tk.Menu(menubar, tearoff=0)
|
||||
menubar.add_cascade(label="Tools", menu=tools_menu)
|
||||
tools_menu.add_command(label="Refresh Models", command=self._refresh_models)
|
||||
tools_menu.add_command(label="Get System Info", command=self._get_system_info)
|
||||
|
||||
# Help menu
|
||||
help_menu = tk.Menu(menubar, tearoff=0)
|
||||
menubar.add_cascade(label="Help", menu=help_menu)
|
||||
help_menu.add_command(label="About", command=self._show_about)
|
||||
|
||||
def _create_connection_frame(self, parent):
|
||||
"""Create connection status frame."""
|
||||
conn_frame = ttk.LabelFrame(parent, text="Connection", padding=10)
|
||||
conn_frame.pack(fill=tk.X, pady=(0, 10))
|
||||
|
||||
# Connection status
|
||||
status_frame = ttk.Frame(conn_frame)
|
||||
status_frame.pack(fill=tk.X)
|
||||
|
||||
ttk.Label(status_frame, text="Status:").pack(side=tk.LEFT)
|
||||
self.connection_status = ttk.Label(status_frame, text="Disconnected", foreground="red")
|
||||
self.connection_status.pack(side=tk.LEFT, padx=(5, 20))
|
||||
|
||||
# Server path
|
||||
ttk.Label(status_frame, text="Server:").pack(side=tk.LEFT)
|
||||
self.server_path_var = tk.StringVar(value="mcp_server.py")
|
||||
self.server_entry = ttk.Entry(status_frame, textvariable=self.server_path_var, width=30)
|
||||
self.server_entry.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# Browse button
|
||||
ttk.Button(status_frame, text="Browse", command=self._browse_server).pack(side=tk.LEFT, padx=2)
|
||||
|
||||
# Connect/Disconnect button
|
||||
self.connect_btn = ttk.Button(status_frame, text="Connect", command=self._toggle_connection)
|
||||
self.connect_btn.pack(side=tk.RIGHT, padx=5)
|
||||
|
||||
def _create_model_tab(self):
|
||||
"""Create model management tab."""
|
||||
model_frame = ttk.Frame(self.notebook)
|
||||
self.notebook.add(model_frame, text="Models")
|
||||
|
||||
# Model list frame
|
||||
list_frame = ttk.LabelFrame(model_frame, text="Available Models", padding=10)
|
||||
list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
# Model tree
|
||||
columns = ("name", "type", "size", "status")
|
||||
self.model_tree = ttk.Treeview(list_frame, columns=columns, show="headings", height=12)
|
||||
|
||||
self.model_tree.heading("name", text="Model Name")
|
||||
self.model_tree.heading("type", text="Type")
|
||||
self.model_tree.heading("size", text="Size")
|
||||
self.model_tree.heading("status", text="Status")
|
||||
|
||||
self.model_tree.column("name", width=300)
|
||||
self.model_tree.column("type", width=100)
|
||||
self.model_tree.column("size", width=100)
|
||||
self.model_tree.column("status", width=100)
|
||||
|
||||
# Scrollbar
|
||||
model_scroll = ttk.Scrollbar(list_frame, orient="vertical", command=self.model_tree.yview)
|
||||
self.model_tree.configure(yscrollcommand=model_scroll.set)
|
||||
|
||||
self.model_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
model_scroll.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
# Model controls
|
||||
control_frame = ttk.Frame(model_frame)
|
||||
control_frame.pack(fill=tk.X, padx=10, pady=10)
|
||||
|
||||
ttk.Button(control_frame, text="Refresh List", command=self._refresh_models).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Button(control_frame, text="Load Model", command=self._load_selected_model).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Button(control_frame, text="Unload Model", command=self._unload_model).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# Current model info
|
||||
current_frame = ttk.LabelFrame(model_frame, text="Current Model", padding=10)
|
||||
current_frame.pack(fill=tk.X, padx=10, pady=10)
|
||||
|
||||
self.current_model_label = ttk.Label(current_frame, text="No model loaded", font=("TkDefaultFont", 10, "bold"))
|
||||
self.current_model_label.pack(anchor=tk.W)
|
||||
|
||||
# Model configuration
|
||||
config_frame = ttk.LabelFrame(model_frame, text="Model Configuration", padding=10)
|
||||
config_frame.pack(fill=tk.X, padx=10, pady=10)
|
||||
|
||||
# Context size
|
||||
ctx_frame = ttk.Frame(config_frame)
|
||||
ctx_frame.pack(fill=tk.X, pady=2)
|
||||
ttk.Label(ctx_frame, text="Context Size:").pack(side=tk.LEFT)
|
||||
self.ctx_var = tk.IntVar(value=4096)
|
||||
ctx_spin = tk.Spinbox(ctx_frame, from_=512, to=32768, increment=512, textvariable=self.ctx_var, width=10)
|
||||
ctx_spin.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# GPU layers
|
||||
gpu_frame = ttk.Frame(config_frame)
|
||||
gpu_frame.pack(fill=tk.X, pady=2)
|
||||
ttk.Label(gpu_frame, text="GPU Layers:").pack(side=tk.LEFT)
|
||||
self.gpu_var = tk.IntVar(value=0)
|
||||
gpu_spin = tk.Spinbox(gpu_frame, from_=0, to=100, increment=1, textvariable=self.gpu_var, width=10)
|
||||
gpu_spin.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
def _create_inference_tab(self):
|
||||
"""Create inference tab."""
|
||||
inference_frame = ttk.Frame(self.notebook)
|
||||
self.notebook.add(inference_frame, text="Inference")
|
||||
|
||||
# Input frame
|
||||
input_frame = ttk.LabelFrame(inference_frame, text="Input", padding=10)
|
||||
input_frame.pack(fill=tk.X, padx=10, pady=10)
|
||||
|
||||
# Prompt input
|
||||
self.prompt_text = scrolledtext.ScrolledText(input_frame, height=6, wrap=tk.WORD)
|
||||
self.prompt_text.pack(fill=tk.X, pady=5)
|
||||
|
||||
# Generation controls
|
||||
controls_frame = ttk.Frame(input_frame)
|
||||
controls_frame.pack(fill=tk.X, pady=5)
|
||||
|
||||
# Max tokens
|
||||
ttk.Label(controls_frame, text="Max Tokens:").pack(side=tk.LEFT)
|
||||
self.max_tokens_var = tk.IntVar(value=256)
|
||||
max_tokens_spin = tk.Spinbox(controls_frame, from_=1, to=8192, increment=16, textvariable=self.max_tokens_var, width=10)
|
||||
max_tokens_spin.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# Temperature
|
||||
ttk.Label(controls_frame, text="Temperature:").pack(side=tk.LEFT, padx=(20, 0))
|
||||
self.temperature_var = tk.DoubleVar(value=0.7)
|
||||
temp_spin = tk.Spinbox(controls_frame, from_=0.0, to=2.0, increment=0.1, textvariable=self.temperature_var, width=10)
|
||||
temp_spin.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# Generate button
|
||||
self.generate_btn = ttk.Button(controls_frame, text="Generate", command=self._generate_text)
|
||||
self.generate_btn.pack(side=tk.RIGHT, padx=5)
|
||||
|
||||
# Output frame
|
||||
output_frame = ttk.LabelFrame(inference_frame, text="Output", padding=10)
|
||||
output_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
self.output_text = scrolledtext.ScrolledText(output_frame, height=15, wrap=tk.WORD, state=tk.DISABLED)
|
||||
self.output_text.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Chat mode
|
||||
chat_frame = ttk.LabelFrame(inference_frame, text="Chat Mode", padding=10)
|
||||
chat_frame.pack(fill=tk.X, padx=10, pady=10)
|
||||
|
||||
chat_controls = ttk.Frame(chat_frame)
|
||||
chat_controls.pack(fill=tk.X)
|
||||
|
||||
self.chat_mode_var = tk.BooleanVar()
|
||||
ttk.Checkbutton(chat_controls, text="Enable Chat Mode", variable=self.chat_mode_var).pack(side=tk.LEFT)
|
||||
|
||||
ttk.Button(chat_controls, text="Clear History", command=self._clear_chat).pack(side=tk.RIGHT)
|
||||
|
||||
def _create_system_tab(self):
|
||||
"""Create system info tab."""
|
||||
system_frame = ttk.Frame(self.notebook)
|
||||
self.notebook.add(system_frame, text="System")
|
||||
|
||||
# System info display
|
||||
info_frame = ttk.LabelFrame(system_frame, text="System Information", padding=10)
|
||||
info_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
self.system_text = scrolledtext.ScrolledText(info_frame, height=20, wrap=tk.WORD, state=tk.DISABLED)
|
||||
self.system_text.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Refresh button
|
||||
ttk.Button(system_frame, text="Refresh System Info", command=self._get_system_info).pack(pady=10)
|
||||
|
||||
def _create_log_tab(self):
|
||||
"""Create log tab."""
|
||||
log_frame = ttk.Frame(self.notebook)
|
||||
self.notebook.add(log_frame, text="Log")
|
||||
|
||||
# Log display
|
||||
self.log_text = scrolledtext.ScrolledText(log_frame, height=25, wrap=tk.WORD, state=tk.DISABLED)
|
||||
self.log_text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
# Log controls
|
||||
log_controls = ttk.Frame(log_frame)
|
||||
log_controls.pack(fill=tk.X, padx=10, pady=10)
|
||||
|
||||
ttk.Button(log_controls, text="Clear Log", command=self._clear_log).pack(side=tk.LEFT)
|
||||
ttk.Button(log_controls, text="Save Log", command=self._save_log).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
def _create_status_bar(self, parent):
|
||||
"""Create status bar."""
|
||||
self.status_bar = ttk.Label(parent, text="Ready", relief=tk.SUNKEN, anchor=tk.W)
|
||||
self.status_bar.pack(fill=tk.X, side=tk.BOTTOM)
|
||||
|
||||
def _log(self, message: str, level: str = "INFO"):
|
||||
"""Add message to log."""
|
||||
timestamp = datetime.now().strftime("%H:%M:%S")
|
||||
log_entry = f"[{timestamp}] {level}: {message}\n"
|
||||
|
||||
self.log_text.config(state=tk.NORMAL)
|
||||
self.log_text.insert(tk.END, log_entry)
|
||||
self.log_text.see(tk.END)
|
||||
self.log_text.config(state=tk.DISABLED)
|
||||
|
||||
def _set_status(self, message: str):
|
||||
"""Set status bar message."""
|
||||
self.status_bar.config(text=message)
|
||||
|
||||
def _connect_dialog(self):
|
||||
"""Show connection dialog."""
|
||||
if self.connected:
|
||||
self._disconnect()
|
||||
else:
|
||||
self._connect()
|
||||
|
||||
def _browse_server(self):
|
||||
"""Browse for server script."""
|
||||
file_path = filedialog.askopenfilename(
|
||||
title="Select MCP Server Script",
|
||||
filetypes=[("Python files", "*.py"), ("All files", "*.*")]
|
||||
)
|
||||
if file_path:
|
||||
self.server_path_var.set(file_path)
|
||||
|
||||
def _toggle_connection(self):
|
||||
"""Toggle connection to server."""
|
||||
if self.connected:
|
||||
self._disconnect()
|
||||
else:
|
||||
self._connect()
|
||||
|
||||
def _connect(self):
|
||||
"""Connect to MCP server."""
|
||||
server_path = self.server_path_var.get().strip()
|
||||
if not server_path:
|
||||
messagebox.showerror("Error", "Please specify server path")
|
||||
return
|
||||
|
||||
if not os.path.exists(server_path):
|
||||
messagebox.showerror("Error", f"Server file not found: {server_path}")
|
||||
return
|
||||
|
||||
self._log(f"Connecting to server: {server_path}")
|
||||
self._set_status("Connecting...")
|
||||
|
||||
# Connect in asyncio thread
|
||||
future = asyncio.run_coroutine_threadsafe(self.client.connect(server_path), self.loop)
|
||||
|
||||
def check_result():
|
||||
if future.done():
|
||||
try:
|
||||
success = future.result()
|
||||
if success:
|
||||
self._log("Connected successfully")
|
||||
else:
|
||||
self._log("Connection failed", "ERROR")
|
||||
except Exception as e:
|
||||
self._log(f"Connection error: {e}", "ERROR")
|
||||
else:
|
||||
self.root.after(100, check_result)
|
||||
|
||||
check_result()
|
||||
|
||||
def _disconnect(self):
|
||||
"""Disconnect from server."""
|
||||
self._log("Disconnecting from server")
|
||||
self.client.disconnect()
|
||||
|
||||
def _on_connect(self):
|
||||
"""Handle successful connection."""
|
||||
self.connected = True
|
||||
self.connection_status.config(text="Connected", foreground="green")
|
||||
self.connect_btn.config(text="Disconnect")
|
||||
self._set_status("Connected to server")
|
||||
|
||||
# Refresh data
|
||||
self._refresh_models()
|
||||
self._get_system_info()
|
||||
|
||||
def _on_disconnect(self):
|
||||
"""Handle disconnection."""
|
||||
self.connected = False
|
||||
self.connection_status.config(text="Disconnected", foreground="red")
|
||||
self.connect_btn.config(text="Connect")
|
||||
self._set_status("Disconnected from server")
|
||||
|
||||
# Clear data
|
||||
self.available_models = []
|
||||
self._update_model_list()
|
||||
|
||||
def _on_error(self, error_message: str):
|
||||
"""Handle client errors."""
|
||||
self._log(error_message, "ERROR")
|
||||
self._set_status(f"Error: {error_message}")
|
||||
|
||||
def _refresh_models(self):
|
||||
"""Refresh the list of available models."""
|
||||
if not self.connected:
|
||||
return
|
||||
|
||||
self._log("Refreshing model list")
|
||||
|
||||
def handle_response(response):
|
||||
if 'error' in response:
|
||||
self._log(f"Error listing models: {response['error']}", "ERROR")
|
||||
return
|
||||
|
||||
try:
|
||||
result = response.get('result', [])
|
||||
if result and isinstance(result, list) and len(result) > 0:
|
||||
content = result[0].get('text', '[]')
|
||||
self.available_models = json.loads(content)
|
||||
else:
|
||||
self.available_models = []
|
||||
|
||||
self.root.after(0, self._update_model_list)
|
||||
self._log(f"Found {len(self.available_models)} models")
|
||||
|
||||
except Exception as e:
|
||||
self._log(f"Error parsing model list: {e}", "ERROR")
|
||||
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
self.client.call_tool("list_models", {}, handle_response),
|
||||
self.loop
|
||||
)
|
||||
|
||||
def _update_model_list(self):
|
||||
"""Update the model list display."""
|
||||
# Clear existing items
|
||||
for item in self.model_tree.get_children():
|
||||
self.model_tree.delete(item)
|
||||
|
||||
# Add models
|
||||
for model in self.available_models:
|
||||
name = model.get('name', 'Unknown')
|
||||
model_type = model.get('type', 'Unknown')
|
||||
size_mb = model.get('size_mb', 0)
|
||||
size_text = f"{size_mb:.1f} MB" if size_mb > 0 else "Unknown"
|
||||
|
||||
self.model_tree.insert("", tk.END, values=(name, model_type, size_text, "Available"))
|
||||
|
||||
def _load_selected_model(self):
|
||||
"""Load the selected model."""
|
||||
selection = self.model_tree.selection()
|
||||
if not selection:
|
||||
messagebox.showinfo("No Selection", "Please select a model to load")
|
||||
return
|
||||
|
||||
item = self.model_tree.item(selection[0])
|
||||
model_name = item['values'][0]
|
||||
|
||||
# Find the model path
|
||||
model_path = None
|
||||
for model in self.available_models:
|
||||
if model.get('name') == model_name:
|
||||
model_path = model.get('path')
|
||||
break
|
||||
|
||||
if not model_path:
|
||||
messagebox.showerror("Error", "Model path not found")
|
||||
return
|
||||
|
||||
self._log(f"Loading model: {model_name}")
|
||||
|
||||
def handle_response(response):
|
||||
if 'error' in response:
|
||||
self._log(f"Error loading model: {response['error']}", "ERROR")
|
||||
return
|
||||
|
||||
try:
|
||||
result = response.get('result', [])
|
||||
if result and isinstance(result, list) and len(result) > 0:
|
||||
content = result[0].get('text', '')
|
||||
if 'Successfully loaded' in content:
|
||||
self.current_model = model_name
|
||||
self.root.after(0, lambda: self.current_model_label.config(text=f"Loaded: {model_name}"))
|
||||
self._log(f"Model loaded successfully: {model_name}")
|
||||
else:
|
||||
self._log(f"Failed to load model: {content}", "ERROR")
|
||||
|
||||
except Exception as e:
|
||||
self._log(f"Error parsing load response: {e}", "ERROR")
|
||||
|
||||
arguments = {
|
||||
"model_path": model_path,
|
||||
"n_ctx": self.ctx_var.get(),
|
||||
"n_gpu_layers": self.gpu_var.get()
|
||||
}
|
||||
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
self.client.call_tool("load_model", arguments, handle_response),
|
||||
self.loop
|
||||
)
|
||||
|
||||
def _unload_model(self):
|
||||
"""Unload the current model."""
|
||||
if not self.current_model:
|
||||
messagebox.showinfo("No Model", "No model is currently loaded")
|
||||
return
|
||||
|
||||
self._log("Unloading current model")
|
||||
self.current_model = None
|
||||
self.current_model_label.config(text="No model loaded")
|
||||
|
||||
def _generate_text(self):
|
||||
"""Generate text using the current model."""
|
||||
if not self.connected:
|
||||
messagebox.showerror("Error", "Not connected to server")
|
||||
return
|
||||
|
||||
if not self.current_model:
|
||||
messagebox.showerror("Error", "No model loaded")
|
||||
return
|
||||
|
||||
prompt = self.prompt_text.get(1.0, tk.END).strip()
|
||||
if not prompt:
|
||||
messagebox.showwarning("Warning", "Please enter a prompt")
|
||||
return
|
||||
|
||||
self._log(f"Generating text for prompt: {prompt[:50]}...")
|
||||
self._set_status("Generating...")
|
||||
|
||||
def handle_response(response):
|
||||
if 'error' in response:
|
||||
self._log(f"Error generating text: {response['error']}", "ERROR")
|
||||
return
|
||||
|
||||
try:
|
||||
result = response.get('result', [])
|
||||
if result and isinstance(result, list) and len(result) > 0:
|
||||
content_str = result[0].get('text', '{}')
|
||||
content = json.loads(content_str)
|
||||
|
||||
if 'error' in content:
|
||||
self._log(f"Generation error: {content['error']}", "ERROR")
|
||||
return
|
||||
|
||||
generated_text = content.get('text', '')
|
||||
|
||||
# Update output
|
||||
self.root.after(0, lambda: self._update_output(prompt, generated_text))
|
||||
self._log("Text generation completed")
|
||||
self.root.after(0, lambda: self._set_status("Generation completed"))
|
||||
|
||||
except Exception as e:
|
||||
self._log(f"Error parsing generation response: {e}", "ERROR")
|
||||
|
||||
arguments = {
|
||||
"prompt": prompt,
|
||||
"max_tokens": self.max_tokens_var.get(),
|
||||
"temperature": self.temperature_var.get()
|
||||
}
|
||||
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
self.client.call_tool("generate_text", arguments, handle_response),
|
||||
self.loop
|
||||
)
|
||||
|
||||
def _update_output(self, prompt: str, generated_text: str):
|
||||
"""Update the output text area."""
|
||||
self.output_text.config(state=tk.NORMAL)
|
||||
|
||||
if self.chat_mode_var.get():
|
||||
# Chat mode - append to conversation
|
||||
self.output_text.insert(tk.END, f"User: {prompt}\n\n")
|
||||
self.output_text.insert(tk.END, f"Assistant: {generated_text}\n\n")
|
||||
self.output_text.insert(tk.END, "-" * 50 + "\n\n")
|
||||
else:
|
||||
# Replace mode - show only current generation
|
||||
self.output_text.delete(1.0, tk.END)
|
||||
self.output_text.insert(tk.END, f"Prompt: {prompt}\n\n")
|
||||
self.output_text.insert(tk.END, f"Response: {generated_text}")
|
||||
|
||||
self.output_text.see(tk.END)
|
||||
self.output_text.config(state=tk.DISABLED)
|
||||
|
||||
def _clear_chat(self):
|
||||
"""Clear chat history."""
|
||||
self.output_text.config(state=tk.NORMAL)
|
||||
self.output_text.delete(1.0, tk.END)
|
||||
self.output_text.config(state=tk.DISABLED)
|
||||
|
||||
def _get_system_info(self):
|
||||
"""Get system information from server."""
|
||||
if not self.connected:
|
||||
return
|
||||
|
||||
self._log("Getting system information")
|
||||
|
||||
def handle_response(response):
|
||||
if 'error' in response:
|
||||
self._log(f"Error getting system info: {response['error']}", "ERROR")
|
||||
return
|
||||
|
||||
try:
|
||||
result = response.get('result', [])
|
||||
if result and isinstance(result, list) and len(result) > 0:
|
||||
content_str = result[0].get('text', '{}')
|
||||
self.system_info = json.loads(content_str)
|
||||
|
||||
self.root.after(0, self._update_system_display)
|
||||
self._log("System information updated")
|
||||
|
||||
except Exception as e:
|
||||
self._log(f"Error parsing system info: {e}", "ERROR")
|
||||
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
self.client.call_tool("get_system_info", {}, handle_response),
|
||||
self.loop
|
||||
)
|
||||
|
||||
def _update_system_display(self):
|
||||
"""Update system information display."""
|
||||
self.system_text.config(state=tk.NORMAL)
|
||||
self.system_text.delete(1.0, tk.END)
|
||||
|
||||
# Format system info
|
||||
info_text = "System Information\n"
|
||||
info_text += "=" * 50 + "\n\n"
|
||||
|
||||
info_text += f"Platform: {self.system_info.get('platform', 'Unknown')}\n"
|
||||
info_text += f"Architecture: {self.system_info.get('architecture', 'Unknown')}\n\n"
|
||||
|
||||
# Acceleration info
|
||||
acceleration = self.system_info.get('acceleration', {})
|
||||
info_text += "Acceleration Support:\n"
|
||||
info_text += f" CUDA Available: {acceleration.get('cuda_available', False)}\n"
|
||||
if acceleration.get('cuda_available'):
|
||||
info_text += f" CUDA Version: {acceleration.get('cuda_version', 'Unknown')}\n"
|
||||
info_text += f" CUDA Devices: {acceleration.get('cuda_devices', 0)}\n"
|
||||
|
||||
info_text += f" ROCm Available: {acceleration.get('rocm_available', False)}\n"
|
||||
info_text += f" Metal Available: {acceleration.get('metal_available', False)}\n"
|
||||
info_text += f" Intel GPU Available: {acceleration.get('intel_gpu_available', False)}\n"
|
||||
info_text += f" Recommended GPU Layers: {acceleration.get('recommended_layers', 0)}\n\n"
|
||||
|
||||
info_text += f"Current Model Acceleration: {self.system_info.get('current_model_acceleration', 'None')}\n"
|
||||
|
||||
self.system_text.insert(tk.END, info_text)
|
||||
self.system_text.config(state=tk.DISABLED)
|
||||
|
||||
def _clear_log(self):
|
||||
"""Clear the log."""
|
||||
self.log_text.config(state=tk.NORMAL)
|
||||
self.log_text.delete(1.0, tk.END)
|
||||
self.log_text.config(state=tk.DISABLED)
|
||||
|
||||
def _save_log(self):
|
||||
"""Save log to file."""
|
||||
file_path = filedialog.asksaveasfilename(
|
||||
title="Save Log",
|
||||
defaultextension=".txt",
|
||||
filetypes=[("Text files", "*.txt"), ("All files", "*.*")]
|
||||
)
|
||||
if file_path:
|
||||
try:
|
||||
with open(file_path, 'w') as f:
|
||||
f.write(self.log_text.get(1.0, tk.END))
|
||||
self._log(f"Log saved to: {file_path}")
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to save log: {e}")
|
||||
|
||||
def _show_about(self):
|
||||
"""Show about dialog."""
|
||||
about_text = """LLM_Train Remote Control v1.0.0
|
||||
|
||||
A remote control application for managing and
|
||||
interacting with LLM_Train MCP servers.
|
||||
|
||||
Features:
|
||||
• Remote model loading and configuration
|
||||
• Text generation and chat interface
|
||||
• System information monitoring
|
||||
• Connection management
|
||||
|
||||
© 2024 LLM_Train Project"""
|
||||
|
||||
messagebox.showinfo("About", about_text)
|
||||
|
||||
def run(self):
|
||||
"""Run the application."""
|
||||
try:
|
||||
self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
|
||||
self.root.mainloop()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
self._cleanup()
|
||||
|
||||
def _on_closing(self):
|
||||
"""Handle application closing."""
|
||||
if self.connected:
|
||||
self.client.disconnect()
|
||||
self._cleanup()
|
||||
self.root.destroy()
|
||||
|
||||
def _cleanup(self):
|
||||
"""Cleanup resources."""
|
||||
if hasattr(self, 'loop') and self.loop.is_running():
|
||||
self.loop.call_soon_threadsafe(self.loop.stop)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
try:
|
||||
app = RemoteControlGUI()
|
||||
app.run()
|
||||
except Exception as e:
|
||||
print(f"Error starting application: {e}")
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user