Fix Hermes LLM GUI startup and layout
This commit is contained in:
+183
-39
@@ -197,18 +197,21 @@ class HermesLLMGUI:
|
||||
body = ttk.PanedWindow(self.root, orient=tk.VERTICAL)
|
||||
body.pack(fill=tk.BOTH, expand=True, padx=6, pady=(0, 6))
|
||||
|
||||
upper = ttk.Frame(body, padding=2)
|
||||
top_area = ttk.PanedWindow(body, orient=tk.VERTICAL)
|
||||
body.add(top_area, weight=5)
|
||||
|
||||
upper = ttk.Frame(top_area, padding=2)
|
||||
upper.columnconfigure(0, weight=1)
|
||||
upper.columnconfigure(1, weight=1)
|
||||
body.add(upper, weight=3)
|
||||
top_area.add(upper, weight=4)
|
||||
|
||||
self._build_llm_panel(upper)
|
||||
self._build_hermes_panel(upper)
|
||||
|
||||
lower = ttk.Frame(body, padding=2)
|
||||
lower = ttk.Frame(top_area, padding=2)
|
||||
lower.columnconfigure(0, weight=1)
|
||||
lower.rowconfigure(0, weight=1)
|
||||
body.add(lower, weight=2)
|
||||
top_area.add(lower, weight=3)
|
||||
|
||||
lower_tabs = ttk.Notebook(lower)
|
||||
lower_tabs.grid(row=0, column=0, sticky=tk.NSEW)
|
||||
@@ -216,27 +219,31 @@ class HermesLLMGUI:
|
||||
actions_tab = ttk.Frame(lower_tabs, padding=4)
|
||||
chat_tab = ttk.Frame(lower_tabs, padding=4)
|
||||
lower_tabs.add(actions_tab, text="Actions")
|
||||
lower_tabs.add(chat_tab, text="Hermes Chat")
|
||||
lower_tabs.add(chat_tab, text="Chat")
|
||||
|
||||
self._build_actions(actions_tab)
|
||||
self._build_chat_tab(chat_tab)
|
||||
|
||||
log_frame = ttk.LabelFrame(self.root, text="Output", padding=4)
|
||||
log_frame.pack(fill=tk.BOTH, expand=True, padx=6, pady=(0, 6))
|
||||
log_frame = ttk.LabelFrame(body, text="Output", padding=4)
|
||||
body.add(log_frame, weight=4)
|
||||
log_frame.columnconfigure(0, weight=1)
|
||||
log_frame.rowconfigure(0, weight=1)
|
||||
|
||||
self.log = scrolledtext.ScrolledText(
|
||||
log_frame,
|
||||
font=("Consolas", 9),
|
||||
font=("Consolas", 10),
|
||||
bg="#1e1e1e",
|
||||
fg="#d4d4d4",
|
||||
insertbackground="white",
|
||||
wrap=tk.WORD,
|
||||
state=tk.DISABLED,
|
||||
height=18,
|
||||
)
|
||||
self.log.pack(fill=tk.BOTH, expand=True)
|
||||
self.log.grid(row=0, column=0, sticky=tk.NSEW)
|
||||
self.log.tag_config("error", foreground="#f48771")
|
||||
self.log.tag_config("ok", foreground="#89d185")
|
||||
self.log.tag_config("info", foreground="#9cdcfe")
|
||||
self.root.after(100, lambda: self._set_initial_pane_sizes(body, top_area))
|
||||
|
||||
def _build_connection_bar(self, parent):
|
||||
frame = ttk.LabelFrame(parent, text="SSH connection", padding=6)
|
||||
@@ -292,15 +299,20 @@ class HermesLLMGUI:
|
||||
ttk.Label(frame, text="Status:").grid(row=len(rows), column=0, sticky=tk.W, padx=4, pady=(8, 3))
|
||||
self.llm_status_label = ttk.Label(frame, textvariable=self.v_llm_status, foreground="#555")
|
||||
self.llm_status_label.grid(row=len(rows), column=1, sticky=tk.W, padx=4, pady=(8, 3))
|
||||
self.llm_status_label.bind("<Double-Button-1>", lambda _event: self._copy_status("LLM", self.v_llm_status.get()))
|
||||
|
||||
buttons = ttk.Frame(frame)
|
||||
buttons.grid(row=len(rows) + 1, column=0, columnspan=3, sticky=tk.W, padx=4, pady=(8, 0))
|
||||
tip(ttk.Button(buttons, text="Start LLM", command=self._start_llm), "Starts the remote llama-server process.").pack(
|
||||
side=tk.LEFT, padx=(0, 6)
|
||||
self.start_llm_button = tip(
|
||||
ttk.Button(buttons, text="Start LLM", command=self._start_llm),
|
||||
"Starts the remote llama-server process.",
|
||||
)
|
||||
tip(ttk.Button(buttons, text="Stop LLM", command=self._stop_llm), "Stops the remote llama-server process.").pack(
|
||||
side=tk.LEFT, padx=6
|
||||
self.start_llm_button.pack(side=tk.LEFT, padx=(0, 6))
|
||||
self.stop_llm_button = tip(
|
||||
ttk.Button(buttons, text="Stop LLM", command=self._stop_llm),
|
||||
"Stops the remote llama-server process.",
|
||||
)
|
||||
self.stop_llm_button.pack(side=tk.LEFT, padx=6)
|
||||
tip(
|
||||
ttk.Button(buttons, text="LLM status", command=self._refresh_llm_status),
|
||||
"Checks the current process state and whether the HTTP endpoint is healthy.",
|
||||
@@ -342,6 +354,10 @@ class HermesLLMGUI:
|
||||
ttk.Label(frame, text="Status:").grid(row=len(rows) + 1, column=0, sticky=tk.W, padx=4, pady=(8, 3))
|
||||
self.hermes_status_label = ttk.Label(frame, textvariable=self.v_hermes_status, foreground="#555")
|
||||
self.hermes_status_label.grid(row=len(rows) + 1, column=1, sticky=tk.W, padx=4, pady=(8, 3))
|
||||
self.hermes_status_label.bind(
|
||||
"<Double-Button-1>",
|
||||
lambda _event: self._copy_status("Hermes", self.v_hermes_status.get()),
|
||||
)
|
||||
|
||||
buttons = ttk.Frame(frame)
|
||||
buttons.grid(row=len(rows) + 2, column=0, columnspan=3, sticky=tk.W, padx=4, pady=(8, 0))
|
||||
@@ -425,13 +441,18 @@ class HermesLLMGUI:
|
||||
buttons = ttk.Frame(parent)
|
||||
buttons.grid(row=1, column=0, sticky=tk.W, pady=(0, 6))
|
||||
tip(
|
||||
ttk.Button(buttons, text="Send to Hermes", command=self._send_chat_message),
|
||||
ttk.Button(buttons, text="Send", command=self._send_chat_message),
|
||||
"Sends the current chat input using the selected mode.",
|
||||
).pack(side=tk.LEFT, padx=(0, 6))
|
||||
tip(
|
||||
ttk.Button(buttons, text="Clear chat", command=self._clear_chat_history),
|
||||
"Clears only the chat tab history in this GUI.",
|
||||
ttk.Button(buttons, text="New chat", command=self._clear_chat_history),
|
||||
"Starts a new local chat session in this GUI.",
|
||||
).pack(side=tk.LEFT, padx=6)
|
||||
ttk.Label(
|
||||
buttons,
|
||||
text="Enter = senden, Shift+Enter = Zeilenumbruch",
|
||||
foreground="#555",
|
||||
).pack(side=tk.LEFT, padx=(12, 0))
|
||||
|
||||
history_frame = ttk.LabelFrame(parent, text="Conversation", padding=4)
|
||||
history_frame.grid(row=2, column=0, sticky=tk.NSEW, pady=(0, 6))
|
||||
@@ -446,21 +467,36 @@ class HermesLLMGUI:
|
||||
height=12,
|
||||
)
|
||||
self.chat_text.grid(row=0, column=0, sticky=tk.NSEW)
|
||||
self.chat_text.tag_config("chat_user", foreground="#0b57d0", spacing1=8, spacing3=10)
|
||||
self.chat_text.tag_config("chat_assistant", foreground="#188038", spacing1=8, spacing3=10)
|
||||
self.chat_text.tag_config("chat_error", foreground="#c5221f", spacing1=8, spacing3=10)
|
||||
self.chat_text.tag_config("chat_meta", foreground="#666666", spacing1=4, spacing3=8)
|
||||
|
||||
input_frame = ttk.Frame(parent)
|
||||
input_frame.grid(row=3, column=0, sticky=tk.EW)
|
||||
input_frame.columnconfigure(0, weight=1)
|
||||
entry = ttk.Entry(input_frame, textvariable=self.v_chat_input)
|
||||
entry.grid(row=0, column=0, sticky=tk.EW, padx=(0, 6))
|
||||
entry.bind("<Return>", lambda _event: self._send_chat_message())
|
||||
tip(
|
||||
self.chat_input = tk.Text(input_frame, height=4, wrap=tk.WORD, font=("Segoe UI", 10))
|
||||
self.chat_input.grid(row=0, column=0, sticky=tk.EW, padx=(0, 6))
|
||||
self.chat_input.bind("<Return>", self._on_chat_return)
|
||||
self.chat_input.bind("<Shift-Return>", self._on_chat_shift_return)
|
||||
self.chat_send_button = tip(
|
||||
ttk.Button(input_frame, text="Send", command=self._send_chat_message),
|
||||
"Submits the current message to Hermes.",
|
||||
).grid(row=0, column=1)
|
||||
"Submits the current message using the selected chat mode.",
|
||||
)
|
||||
self.chat_send_button.grid(row=0, column=1, sticky=tk.N)
|
||||
self._append_chat_meta("Neuer Chat gestartet.")
|
||||
|
||||
def _submit(self, coro):
|
||||
return asyncio.run_coroutine_threadsafe(coro, self._loop)
|
||||
|
||||
def _set_initial_pane_sizes(self, outer_pane: ttk.PanedWindow, top_pane: ttk.PanedWindow):
|
||||
try:
|
||||
total = max(self.root.winfo_height(), 760)
|
||||
outer_pane.sashpos(0, int(total * 0.62))
|
||||
top_pane.sashpos(0, int(total * 0.36))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _make_ssh(self) -> AsyncSSH:
|
||||
return AsyncSSH(
|
||||
self.v_host.get().strip(),
|
||||
@@ -533,21 +569,29 @@ class HermesLLMGUI:
|
||||
return f"""
|
||||
set -e
|
||||
mkdir -p {shlex.quote(os.path.dirname(log_path))} {shlex.quote(os.path.dirname(pid_path))}
|
||||
pkill -f "llama-server.*{model}" || true
|
||||
if [ -f {shlex.quote(pid_path)} ]; then
|
||||
oldpid=$(cat {shlex.quote(pid_path)})
|
||||
if ps -p "$oldpid" > /dev/null 2>&1; then
|
||||
kill "$oldpid" 2>/dev/null || true
|
||||
sleep 1
|
||||
fi
|
||||
rm -f {shlex.quote(pid_path)}
|
||||
fi
|
||||
pkill -x llama-server 2>/dev/null || true
|
||||
cd {shlex.quote(workdir)}
|
||||
nohup ./build/bin/llama-server \\
|
||||
setsid nohup ./build/bin/llama-server \\
|
||||
-m {shlex.quote(model)} \\
|
||||
-a {shlex.quote(alias)} \\
|
||||
-ngl 0 -t {threads} -c {ctx} --host {shlex.quote(bind_host)} --port {port} \\
|
||||
> {shlex.quote(log_path)} 2>&1 &
|
||||
echo $! > {shlex.quote(pid_path)}
|
||||
< /dev/null >> {shlex.quote(log_path)} 2>&1 &
|
||||
pid=$!
|
||||
echo "$pid" > {shlex.quote(pid_path)}
|
||||
sleep 3
|
||||
cat {shlex.quote(pid_path)}
|
||||
"""
|
||||
|
||||
def _llm_stop_script(self) -> str:
|
||||
pid_path = self.v_llm_pid.get().strip()
|
||||
model = self.v_llm_model.get().strip()
|
||||
return f"""
|
||||
set -e
|
||||
if [ -f {shlex.quote(pid_path)} ]; then
|
||||
@@ -555,12 +599,13 @@ if [ -f {shlex.quote(pid_path)} ]; then
|
||||
kill "$pid" 2>/dev/null || true
|
||||
rm -f {shlex.quote(pid_path)}
|
||||
fi
|
||||
pkill -f "llama-server.*{model}" || true
|
||||
pkill -x llama-server 2>/dev/null || true
|
||||
"""
|
||||
|
||||
def _llm_status_script(self) -> str:
|
||||
pid_path = self.v_llm_pid.get().strip()
|
||||
port = int(self.v_llm_port.get())
|
||||
host = self.v_host.get().strip()
|
||||
return f"""
|
||||
if [ -f {shlex.quote(pid_path)} ]; then
|
||||
pid=$(cat {shlex.quote(pid_path)})
|
||||
@@ -568,13 +613,14 @@ if [ -f {shlex.quote(pid_path)} ]; then
|
||||
echo "running: pid=$pid"
|
||||
ps -p "$pid" -o pid=,etime=,cmd=
|
||||
else
|
||||
echo "stale pid file: $pid"
|
||||
rm -f {shlex.quote(pid_path)}
|
||||
echo "not running (removed stale pid: $pid)"
|
||||
fi
|
||||
else
|
||||
echo "not running"
|
||||
fi
|
||||
printf '%s\\n' '--- health ---'
|
||||
curl -s http://127.0.0.1:{port}/health || true
|
||||
curl -s http://{host}:{port}/health || true
|
||||
"""
|
||||
|
||||
def _hermes_start_script(self) -> str:
|
||||
@@ -594,8 +640,13 @@ if [ -f {shlex.quote(pid_path)} ]; then
|
||||
rm -f {shlex.quote(pid_path)}
|
||||
fi
|
||||
. {shlex.quote(posixpath.join(venv, "bin", "activate"))}
|
||||
nohup hermes serve --host {shlex.quote(bind_host)} --port {port}{extra_suffix} > {shlex.quote(log_path)} 2>&1 &
|
||||
echo $! > {shlex.quote(pid_path)}
|
||||
setsid bash -lc {shlex.quote(
|
||||
f". {shlex.quote(posixpath.join(venv, 'bin', 'activate'))} && "
|
||||
+ f"hermes serve --host {shlex.quote(bind_host)} --port {port}{extra_suffix} "
|
||||
+ f"< /dev/null >> {shlex.quote(log_path)} 2>&1"
|
||||
)} &
|
||||
pid=$!
|
||||
echo "$pid" > {shlex.quote(pid_path)}
|
||||
sleep 3
|
||||
cat {shlex.quote(pid_path)}
|
||||
"""
|
||||
@@ -634,11 +685,12 @@ curl -sI http://127.0.0.1:{port} | head -1 || true
|
||||
|
||||
def _start_llm(self):
|
||||
self._log("[info] starting remote llama.cpp server\n", "info")
|
||||
self._submit(self._async_run_script(self._llm_start_script(), "LLM started", self._refresh_llm_status))
|
||||
self._set_llm_starting(True)
|
||||
self._submit(self._async_start_llm())
|
||||
|
||||
def _stop_llm(self):
|
||||
self._log("[info] stopping remote llama.cpp server\n", "info")
|
||||
self._submit(self._async_run_script(self._llm_stop_script(), "LLM stopped", self._refresh_llm_status))
|
||||
self._submit(self._async_stop_llm())
|
||||
|
||||
def _start_hermes(self):
|
||||
self._log("[info] starting remote Hermes backend\n", "info")
|
||||
@@ -658,14 +710,14 @@ curl -sI http://127.0.0.1:{port} | head -1 || true
|
||||
|
||||
async def _async_start_both(self):
|
||||
self._log("[info] starting LLM first\n", "info")
|
||||
await self._async_run_script(self._llm_start_script(), "LLM started")
|
||||
await self._async_start_llm()
|
||||
self._log("[info] starting Hermes next\n", "info")
|
||||
await self._async_run_script(self._hermes_start_script(), "Hermes started")
|
||||
self.root.after(0, self._refresh_all_status)
|
||||
|
||||
async def _async_stop_both(self):
|
||||
await self._async_run_script(self._hermes_stop_script(), "Hermes stopped")
|
||||
await self._async_run_script(self._llm_stop_script(), "LLM stopped")
|
||||
await self._async_stop_llm()
|
||||
self.root.after(0, self._refresh_all_status)
|
||||
|
||||
def _refresh_llm_status(self):
|
||||
@@ -697,6 +749,47 @@ curl -sI http://127.0.0.1:{port} | head -1 || true
|
||||
finally:
|
||||
await ssh.close()
|
||||
|
||||
async def _async_start_llm(self):
|
||||
ssh = self._make_ssh()
|
||||
try:
|
||||
await ssh.connect()
|
||||
result = await ssh.exec(f"bash -lc {shlex.quote(self._llm_start_script())}")
|
||||
if result.strip():
|
||||
self._log(result if result.endswith("\n") else result + "\n", "info")
|
||||
|
||||
llm_port = int(self.v_llm_port.get())
|
||||
llm_host = self.v_host.get().strip()
|
||||
health_cmd = "bash -lc " + shlex.quote(f"curl -s http://{llm_host}:{llm_port}/health")
|
||||
|
||||
for _ in range(20):
|
||||
health_output = await ssh.exec(health_cmd)
|
||||
if '"status":"ok"' in health_output.replace(" ", ""):
|
||||
self._log(f"[ok] LLM started and healthy on {llm_host}:{llm_port}\n", "ok")
|
||||
self.root.after(0, lambda: self._set_llm_status("running"))
|
||||
return
|
||||
await asyncio.sleep(1)
|
||||
|
||||
self._log(
|
||||
f"[error] LLM start command ran, but no healthy endpoint appeared on {llm_host}:{llm_port}\n",
|
||||
"error",
|
||||
)
|
||||
log_output = await ssh.exec(
|
||||
f"bash -lc {shlex.quote(f'tail -n 60 {shlex.quote(self.v_llm_log.get().strip())}')}"
|
||||
)
|
||||
if log_output.strip():
|
||||
self._log(log_output if log_output.endswith("\n") else log_output + "\n", "error")
|
||||
self.root.after(0, lambda: self._set_llm_status("not running"))
|
||||
except Exception as exc:
|
||||
self._log(f"[error] LLM start failed: {exc}\n", "error")
|
||||
self.root.after(0, lambda: self._set_llm_status("not running"))
|
||||
finally:
|
||||
await ssh.close()
|
||||
self.root.after(0, lambda: self._set_llm_starting(False))
|
||||
self.root.after(0, self._refresh_llm_status)
|
||||
|
||||
async def _async_stop_llm(self):
|
||||
await self._async_run_script(self._llm_stop_script(), "LLM stopped", self._refresh_llm_status)
|
||||
|
||||
def _tail_llm_log(self):
|
||||
self._submit(self._async_tail_log(self.v_llm_log.get().strip(), "LLM"))
|
||||
|
||||
@@ -723,10 +816,13 @@ curl -sI http://127.0.0.1:{port} | head -1 || true
|
||||
self._submit(self._async_test_chat())
|
||||
|
||||
def _send_chat_message(self):
|
||||
text = self.v_chat_input.get().strip()
|
||||
if hasattr(self, "chat_send_button") and str(self.chat_send_button["state"]) == "disabled":
|
||||
return
|
||||
text = self.chat_input.get("1.0", tk.END).strip()
|
||||
if not text:
|
||||
return
|
||||
self.v_chat_input.set("")
|
||||
self.chat_input.delete("1.0", tk.END)
|
||||
self._set_chat_busy(True)
|
||||
self._append_chat_message("You", text)
|
||||
self._chat_history.append(("user", text))
|
||||
if self.v_chat_mode.get() == "hermes":
|
||||
@@ -739,13 +835,50 @@ curl -sI http://127.0.0.1:{port} | head -1 || true
|
||||
self.chat_text.config(state=tk.NORMAL)
|
||||
self.chat_text.delete("1.0", tk.END)
|
||||
self.chat_text.config(state=tk.DISABLED)
|
||||
if hasattr(self, "chat_input"):
|
||||
self.chat_input.delete("1.0", tk.END)
|
||||
self._append_chat_meta("Neuer Chat gestartet.")
|
||||
|
||||
def _append_chat_message(self, speaker: str, text: str):
|
||||
tag = "chat_assistant"
|
||||
if speaker == "You":
|
||||
tag = "chat_user"
|
||||
elif "Error" in speaker:
|
||||
tag = "chat_error"
|
||||
self.chat_text.config(state=tk.NORMAL)
|
||||
self.chat_text.insert(tk.END, f"{speaker}:\n{text.strip()}\n\n")
|
||||
self.chat_text.insert(tk.END, f"{speaker}:\n", tag)
|
||||
self.chat_text.insert(tk.END, f"{text.strip()}\n\n")
|
||||
self.chat_text.see(tk.END)
|
||||
self.chat_text.config(state=tk.DISABLED)
|
||||
|
||||
def _append_chat_meta(self, text: str):
|
||||
self.chat_text.config(state=tk.NORMAL)
|
||||
self.chat_text.insert(tk.END, f"{text.strip()}\n\n", "chat_meta")
|
||||
self.chat_text.see(tk.END)
|
||||
self.chat_text.config(state=tk.DISABLED)
|
||||
|
||||
def _copy_status(self, label: str, value: str):
|
||||
text = f"{label} status: {value or 'unknown'}"
|
||||
self.root.clipboard_clear()
|
||||
self.root.clipboard_append(text)
|
||||
self._log(f"[info] copied to clipboard: {text}\n", "info")
|
||||
|
||||
def _set_chat_busy(self, busy: bool):
|
||||
if hasattr(self, "chat_send_button"):
|
||||
self.chat_send_button.configure(state=tk.DISABLED if busy else tk.NORMAL)
|
||||
if hasattr(self, "chat_input"):
|
||||
self.chat_input.configure(state=tk.DISABLED if busy else tk.NORMAL)
|
||||
if not busy:
|
||||
self.chat_input.focus_set()
|
||||
|
||||
def _on_chat_return(self, _event):
|
||||
self._send_chat_message()
|
||||
return "break"
|
||||
|
||||
def _on_chat_shift_return(self, _event):
|
||||
self.chat_input.insert(tk.INSERT, "\n")
|
||||
return "break"
|
||||
|
||||
def _set_llm_status(self, text: str):
|
||||
self.v_llm_status.set(text)
|
||||
color = "#555"
|
||||
@@ -756,6 +889,15 @@ curl -sI http://127.0.0.1:{port} | head -1 || true
|
||||
color = "#c5221f"
|
||||
self.llm_status_label.configure(foreground=color)
|
||||
|
||||
def _set_llm_starting(self, starting: bool):
|
||||
if hasattr(self, "start_llm_button"):
|
||||
self.start_llm_button.configure(
|
||||
text="Starting..." if starting else "Start LLM",
|
||||
state=tk.DISABLED if starting else tk.NORMAL,
|
||||
)
|
||||
if hasattr(self, "stop_llm_button"):
|
||||
self.stop_llm_button.configure(state=tk.DISABLED if starting else tk.NORMAL)
|
||||
|
||||
def _set_hermes_status(self, text: str):
|
||||
self.v_hermes_status.set(text)
|
||||
color = "#555"
|
||||
@@ -917,6 +1059,7 @@ curl -sI http://127.0.0.1:{port} | head -1 || true
|
||||
self._log(f"[error] Hermes chat failed: {exc}\n", "error")
|
||||
finally:
|
||||
await ssh.close()
|
||||
self.root.after(0, lambda: self._set_chat_busy(False))
|
||||
|
||||
async def _async_send_direct_llm_chat_message(self):
|
||||
ssh = self._make_ssh()
|
||||
@@ -979,6 +1122,7 @@ curl -sI http://127.0.0.1:{port} | head -1 || true
|
||||
self._log(f"[error] LLM chat failed: {exc}\n", "error")
|
||||
finally:
|
||||
await ssh.close()
|
||||
self.root.after(0, lambda: self._set_chat_busy(False))
|
||||
|
||||
async def _async_run_script(self, script: str, ok_message: str = "", callback=None):
|
||||
ssh = self._make_ssh()
|
||||
|
||||
Reference in New Issue
Block a user