907 lines
34 KiB
Python
907 lines
34 KiB
Python
|
|
#!/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())
|