490 lines
20 KiB
Python
490 lines
20 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import threading
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Dict, List, Tuple
|
|
from tkinter import filedialog, messagebox, scrolledtext, ttk
|
|
import tkinter as tk
|
|
|
|
CORE_DIR = Path(__file__).resolve().parent / ".core"
|
|
if str(CORE_DIR) not in sys.path:
|
|
sys.path.insert(0, str(CORE_DIR))
|
|
|
|
from config import RESULTS_DIR, TESTS_DIR # type: ignore # noqa: E402
|
|
from providers import ( # type: ignore # noqa: E402
|
|
LocalEngineProvider,
|
|
ProviderError,
|
|
get_provider,
|
|
list_provider_names,
|
|
)
|
|
from reporting import TemplateNotFoundError # type: ignore # noqa: E402
|
|
from runner import TestRunError, run_suite # type: ignore # noqa: E402
|
|
from settings import get_local_model_paths, set_local_model_paths # type: ignore # noqa: E402
|
|
|
|
|
|
def open_path(path: Path) -> None:
|
|
if sys.platform.startswith("darwin"):
|
|
subprocess.run(["open", str(path)], check=False)
|
|
elif os.name == "nt": # Windows
|
|
os.startfile(str(path)) # type: ignore[attr-defined]
|
|
else:
|
|
subprocess.run(["xdg-open", str(path)], check=False)
|
|
|
|
|
|
class App(tk.Tk):
|
|
def open_settings_dialog(self):
|
|
try:
|
|
from settings_dialog import SettingsDialog
|
|
except ImportError:
|
|
messagebox.showerror("Error", "Settings dialog module not found.")
|
|
return
|
|
SettingsDialog(self)
|
|
def open_prompt_playground(self):
|
|
try:
|
|
from prompt_playground import PlaygroundDialog
|
|
except ImportError:
|
|
messagebox.showerror("Error", "Prompt playground module not found.")
|
|
return
|
|
def send_callback(prompt):
|
|
provider_name = self.provider_var.get()
|
|
model_display = self.model_var.get()
|
|
if not provider_name or not model_display:
|
|
return "Please select a provider and model."
|
|
try:
|
|
provider = get_provider(provider_name, **self._provider_kwargs(provider_name))
|
|
model_id = self._model_id_for_display(model_display)
|
|
if not model_id:
|
|
return "Model not found."
|
|
# Use temperature from the GUI
|
|
response = provider.run_inference(model_id, prompt, temperature=self.temperature_var.get())
|
|
return response
|
|
except Exception as e:
|
|
return f"Error: {e}"
|
|
PlaygroundDialog(self, send_callback)
|
|
def load_plugins(self):
|
|
try:
|
|
from plugin_system import PluginManager
|
|
self.plugin_manager = PluginManager()
|
|
self.plugin_manager.run_hook('on_app_start', self)
|
|
except Exception as e:
|
|
print(f"Plugin system error: {e}")
|
|
def open_results_chart(self):
|
|
try:
|
|
from results_chart import ResultsChartDialog
|
|
except ImportError:
|
|
messagebox.showerror("Error", "Results chart module not found.")
|
|
return
|
|
# Find the latest report
|
|
if not RESULTS_DIR.exists():
|
|
messagebox.showinfo("No Reports", "The results directory does not exist yet.")
|
|
return
|
|
reports = list(RESULTS_DIR.glob("*.md"))
|
|
if not reports:
|
|
messagebox.showinfo("No Reports", "No markdown reports found yet.")
|
|
return
|
|
latest_report = max(reports, key=lambda p: p.stat().st_mtime)
|
|
ResultsChartDialog(self, latest_report)
|
|
def open_diff_viewer(self):
|
|
try:
|
|
from diff_viewer import DiffViewerDialog
|
|
except ImportError:
|
|
messagebox.showerror("Error", "Diff viewer module not found.")
|
|
return
|
|
# Try to get the last test's expected and actual output from the log or a result file
|
|
# For now, just prompt the user for both (can be improved to auto-load from last run)
|
|
expected = ""
|
|
actual = ""
|
|
def get_text(title):
|
|
d = tk.Toplevel(self)
|
|
d.title(f"{title} - LM-Gambit")
|
|
d.geometry("600x400")
|
|
t = tk.Text(d, wrap="word")
|
|
t.pack(fill="both", expand=True)
|
|
result = {}
|
|
def ok():
|
|
result['text'] = t.get("1.0", "end-1c")
|
|
d.destroy()
|
|
ttk.Button(d, text="OK", command=ok).pack()
|
|
self.wait_window(d)
|
|
return result.get('text', "")
|
|
expected = get_text("Paste Expected Output")
|
|
actual = get_text("Paste Actual Output")
|
|
if expected and actual:
|
|
DiffViewerDialog(self, expected, actual)
|
|
else:
|
|
messagebox.showinfo("Diff Viewer", "Both expected and actual output are required.")
|
|
def configure_model_paths(self) -> None:
|
|
dialog = tk.Toplevel(self)
|
|
dialog.title("Configure Model Paths - LM-Gambit")
|
|
dialog.geometry("520x320")
|
|
dialog.transient(self)
|
|
dialog.grab_set()
|
|
|
|
paths: List[str] = get_local_model_paths()
|
|
|
|
listbox = tk.Listbox(dialog, selectmode=tk.SINGLE, width=60, height=10)
|
|
for entry in paths:
|
|
listbox.insert("end", entry)
|
|
listbox.pack(fill="both", expand=True, padx=12, pady=(12, 6))
|
|
|
|
button_frame = ttk.Frame(dialog)
|
|
button_frame.pack(fill="x", padx=12, pady=6)
|
|
|
|
def add_path() -> None:
|
|
selection = filedialog.askdirectory(parent=dialog)
|
|
if selection and selection not in paths:
|
|
paths.append(selection)
|
|
listbox.insert("end", selection)
|
|
|
|
def remove_path() -> None:
|
|
selection = listbox.curselection()
|
|
if not selection:
|
|
return
|
|
index = selection[0]
|
|
listbox.delete(index)
|
|
paths.pop(index)
|
|
|
|
ttk.Button(button_frame, text="Add Path", command=add_path).pack(side="left", padx=(0, 8))
|
|
ttk.Button(button_frame, text="Remove Selected", command=remove_path).pack(side="left")
|
|
ttk.Button(button_frame, text="Save", command=lambda: (set_local_model_paths(paths), dialog.destroy())).pack(side="right")
|
|
def open_test_editor(self):
|
|
try:
|
|
from test_editor import TestEditorDialog
|
|
except ImportError:
|
|
messagebox.showerror("Error", "Test editor module not found.")
|
|
return
|
|
TestEditorDialog(self, TESTS_DIR)
|
|
|
|
def open_latest_report(self) -> None:
|
|
if not RESULTS_DIR.exists():
|
|
messagebox.showinfo("No Reports", "The results directory does not exist yet.")
|
|
return
|
|
reports = list(RESULTS_DIR.glob("*.md"))
|
|
if not reports:
|
|
messagebox.showinfo("No Reports", "No markdown reports found yet.")
|
|
return
|
|
latest_report = max(reports, key=lambda p: p.stat().st_mtime)
|
|
open_path(latest_report)
|
|
self._append_log(f"Opened latest report: {latest_report.name}")
|
|
def open_model_downloader(self):
|
|
try:
|
|
from model_downloader import DownloadModelDialog
|
|
except ImportError:
|
|
messagebox.showerror("Error", "Model downloader module not found.")
|
|
return
|
|
paths = get_local_model_paths()
|
|
import os
|
|
model_dir = Path(paths[0]).expanduser() if paths else (Path(os.getcwd()) / "models")
|
|
model_dir.mkdir(parents=True, exist_ok=True)
|
|
DownloadModelDialog(self, model_dir)
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.title("LM-Gambit v1.0.0")
|
|
self.geometry("880x620")
|
|
self.minsize(760, 520)
|
|
|
|
self.provider_names = list_provider_names()
|
|
# Always default to Local Engine if available
|
|
default_provider = LocalEngineProvider.name if LocalEngineProvider.name in self.provider_names else (self.provider_names[0] if self.provider_names else "")
|
|
self.provider_var = tk.StringVar(value=default_provider)
|
|
self.model_var = tk.StringVar()
|
|
self.temperature_var = tk.DoubleVar(value=0.1)
|
|
self.status_var = tk.StringVar(value="Idle")
|
|
|
|
self.available_models: List[Tuple[str, str]] = [] # (id, display name)
|
|
self.running = False
|
|
|
|
self.load_plugins()
|
|
|
|
self._build_ui()
|
|
self._build_menu()
|
|
if self.provider_var.get():
|
|
self.fetch_models_async()
|
|
|
|
def _build_ui(self) -> None:
|
|
padding = {"padx": 12, "pady": 8}
|
|
|
|
header = ttk.Label(self, text="Automated Diagnostic Test Runner", font=("Helvetica", 18, "bold"))
|
|
header.pack(anchor="w", **padding)
|
|
|
|
form_frame = ttk.Frame(self)
|
|
form_frame.pack(fill="x", **padding)
|
|
|
|
ttk.Label(form_frame, text="Provider:").grid(row=0, column=0, sticky="w")
|
|
self.provider_combo = ttk.Combobox(
|
|
form_frame,
|
|
textvariable=self.provider_var,
|
|
values=self.provider_names,
|
|
state="readonly",
|
|
)
|
|
self.provider_combo.grid(row=0, column=1, sticky="ew", padx=(6, 18))
|
|
self.provider_combo.bind("<<ComboboxSelected>>", lambda event: self.fetch_models_async())
|
|
|
|
ttk.Label(form_frame, text="Model:").grid(row=0, column=2, sticky="w")
|
|
self.model_combo = ttk.Combobox(form_frame, textvariable=self.model_var, state="readonly")
|
|
self.model_combo.grid(row=0, column=3, sticky="ew", padx=(6, 18))
|
|
|
|
ttk.Label(form_frame, text="Temperature:").grid(row=0, column=4, sticky="w")
|
|
self.temperature_spin = ttk.Spinbox(
|
|
form_frame,
|
|
textvariable=self.temperature_var,
|
|
from_=0.0,
|
|
to=1.0,
|
|
increment=0.05,
|
|
width=6,
|
|
)
|
|
self.temperature_spin.grid(row=0, column=5, sticky="w")
|
|
|
|
form_frame.columnconfigure(1, weight=1)
|
|
form_frame.columnconfigure(3, weight=1)
|
|
|
|
button_frame = ttk.Frame(self)
|
|
button_frame.pack(fill="x", **padding)
|
|
|
|
self.run_button = ttk.Button(button_frame, text="Run Tests", command=self.start_run)
|
|
self.run_button.pack(side="left")
|
|
|
|
ttk.Button(button_frame, text="Refresh Models", command=self.fetch_models_async).pack(side="left", padx=(12, 0))
|
|
ttk.Button(button_frame, text="New Test", command=self.open_test_editor).pack(side="left", padx=(12, 0))
|
|
ttk.Button(button_frame, text="Compare Outputs", command=self.open_diff_viewer).pack(side="left", padx=(12, 0))
|
|
ttk.Button(button_frame, text="View Results Chart", command=self.open_results_chart).pack(side="left", padx=(12, 0))
|
|
ttk.Button(button_frame, text="Prompt Playground", command=self.open_prompt_playground).pack(side="left", padx=(12, 0))
|
|
ttk.Button(button_frame, text="Settings", command=self.open_settings_dialog).pack(side="left", padx=(12, 0))
|
|
ttk.Button(button_frame, text="Download Model", command=self.open_model_downloader).pack(side="left", padx=(12, 0))
|
|
ttk.Button(button_frame, text="Edit Tests", command=lambda: open_path(TESTS_DIR)).pack(side="left", padx=(12, 0))
|
|
ttk.Button(button_frame, text="Open Results", command=lambda: open_path(RESULTS_DIR)).pack(side="left", padx=(12, 0))
|
|
ttk.Button(button_frame, text="View Latest Report", command=self.open_latest_report).pack(side="left", padx=(12, 0))
|
|
|
|
ttk.Label(self, textvariable=self.status_var).pack(anchor="w", **padding)
|
|
|
|
self.log = scrolledtext.ScrolledText(self, wrap="word", height=22)
|
|
self.log.pack(fill="both", expand=True, padx=12, pady=(0, 12))
|
|
self.log.configure(state="disabled")
|
|
|
|
def _build_menu(self) -> None:
|
|
menubar = tk.Menu(self)
|
|
settings_menu = tk.Menu(menubar, tearoff=0)
|
|
settings_menu.add_command(label="Settings…", command=self.open_settings_dialog)
|
|
menubar.add_cascade(label="Settings", menu=settings_menu)
|
|
self.config(menu=menubar)
|
|
self._settings_menu = settings_menu
|
|
|
|
def fetch_models_async(self) -> None:
|
|
if self.running:
|
|
return
|
|
|
|
provider_name = self.provider_var.get()
|
|
if not provider_name:
|
|
return
|
|
|
|
self._set_status(f"Fetching models for {provider_name}…")
|
|
self.model_combo.configure(values=())
|
|
self.model_var.set("")
|
|
|
|
thread = threading.Thread(target=self._fetch_models, daemon=True)
|
|
thread.start()
|
|
|
|
def _fetch_models(self) -> None:
|
|
provider_name = self.provider_var.get()
|
|
try:
|
|
provider = get_provider(provider_name, **self._provider_kwargs(provider_name))
|
|
models = provider.list_models()
|
|
except ProviderError as exc:
|
|
message = str(exc)
|
|
self.after(0, lambda m=message: self._on_model_fetch_error(m))
|
|
return
|
|
|
|
model_pairs = [(model.id, model.display_name) for model in models]
|
|
self.after(0, lambda: self._on_models_loaded(model_pairs))
|
|
|
|
def open_test_editor(self):
|
|
try:
|
|
from test_editor import TestEditorDialog
|
|
except ImportError:
|
|
messagebox.showerror("Error", "Test editor module not found.")
|
|
return
|
|
TestEditorDialog(self, TESTS_DIR)
|
|
def _on_model_fetch_error(self, message: str) -> None:
|
|
self._set_status("Model fetch failed")
|
|
messagebox.showerror("Model Load Error", message)
|
|
|
|
def _on_models_loaded(self, model_pairs: List[Tuple[str, str]]) -> None:
|
|
self.available_models = model_pairs
|
|
options = [display for _, display in model_pairs]
|
|
self.model_combo.configure(values=options)
|
|
if options:
|
|
self.model_var.set(options[0])
|
|
self._set_status(f"Loaded {len(options)} models.")
|
|
else:
|
|
self.model_var.set("")
|
|
self._set_status("No models found.")
|
|
|
|
def start_run(self) -> None:
|
|
if self.running:
|
|
return
|
|
|
|
provider_name = self.provider_var.get()
|
|
model_display = self.model_var.get()
|
|
|
|
if not provider_name:
|
|
messagebox.showwarning("Missing Provider", "Please select a provider before running tests.")
|
|
return
|
|
|
|
if not model_display:
|
|
messagebox.showwarning("Missing Model", "Please select a model before running tests.")
|
|
return
|
|
|
|
model_id = self._model_id_for_display(model_display)
|
|
if not model_id:
|
|
messagebox.showerror("Model Error", "Unable to resolve the selected model.")
|
|
return
|
|
|
|
temperature = float(self.temperature_var.get())
|
|
self.running = True
|
|
self._set_status("Running tests…")
|
|
self._append_log(f"Starting run with provider '{provider_name}' and model '{model_display}'.")
|
|
self._toggle_controls(state="disabled")
|
|
|
|
thread = threading.Thread(
|
|
target=self._run_tests_thread,
|
|
args=(provider_name, model_id, temperature),
|
|
daemon=True,
|
|
)
|
|
thread.start()
|
|
|
|
def _run_tests_thread(self, provider_name: str, model_id: str, temperature: float) -> None:
|
|
start_time = time.time()
|
|
|
|
def progress_callback(index: int, total: int, prompt: Dict[str, str], result: Dict[str, object]) -> None:
|
|
status = "FAILED" if "error" in result else "DONE"
|
|
title = prompt.get("title", f"Test {index}")
|
|
filename = prompt.get("filename", f"test{index}")
|
|
message = f"[{index}/{total}] {title} ({filename}) -> {status}"
|
|
self.after(0, lambda: self._append_log(message))
|
|
|
|
try:
|
|
report_path = run_suite(
|
|
provider_name=provider_name,
|
|
model_id=model_id,
|
|
temperature=temperature,
|
|
progress_callback=progress_callback,
|
|
)
|
|
except (TestRunError, TemplateNotFoundError) as exc:
|
|
message = str(exc)
|
|
self.after(0, lambda m=message: self._handle_run_error(m))
|
|
return
|
|
|
|
elapsed = time.time() - start_time
|
|
self.after(0, lambda: self._handle_run_success(report_path, elapsed))
|
|
|
|
def _handle_run_error(self, message: str) -> None:
|
|
self.running = False
|
|
self._toggle_controls(state="normal")
|
|
self._set_status("Run failed")
|
|
self._append_log(f"Error: {message}")
|
|
messagebox.showerror("Run Failed", message)
|
|
|
|
def _handle_run_success(self, report_path: Path, elapsed: float) -> None:
|
|
self.running = False
|
|
self._toggle_controls(state="normal")
|
|
self._set_status("Run complete")
|
|
self._append_log(f"Run complete in {elapsed:.2f}s. Report saved to {report_path.name}.")
|
|
messagebox.showinfo("Run Complete", f"Report saved to {report_path}")
|
|
|
|
def _toggle_controls(self, *, state: str) -> None:
|
|
widgets = [
|
|
self.run_button,
|
|
self.provider_combo,
|
|
self.model_combo,
|
|
self.temperature_spin,
|
|
]
|
|
for widget in widgets:
|
|
widget.configure(state=state)
|
|
|
|
def _append_log(self, message: str) -> None:
|
|
timestamp = time.strftime("%H:%M:%S")
|
|
self.log.configure(state="normal")
|
|
self.log.insert("end", f"[{timestamp}] {message}\n")
|
|
self.log.see("end")
|
|
self.log.configure(state="disabled")
|
|
|
|
def _set_status(self, message: str) -> None:
|
|
self.status_var.set(message)
|
|
|
|
def _model_id_for_display(self, display_name: str) -> str | None:
|
|
for model_id, display in self.available_models:
|
|
if display == display_name:
|
|
return model_id
|
|
return None
|
|
|
|
def _provider_kwargs(self, provider_name: str) -> Dict[str, object]:
|
|
if provider_name == LocalEngineProvider.name:
|
|
custom_paths = [Path(p).expanduser() for p in get_local_model_paths()]
|
|
return {"search_paths": [path for path in custom_paths if path]}
|
|
return {}
|
|
|
|
def _build_ui(self) -> None:
|
|
padding = {"padx": 12, "pady": 8}
|
|
|
|
header = ttk.Label(self, text="Automated Diagnostic Test Runner", font=("Helvetica", 18, "bold"))
|
|
header.pack(anchor="w", **padding)
|
|
|
|
form_frame = ttk.Frame(self)
|
|
form_frame.pack(fill="x", **padding)
|
|
|
|
ttk.Label(form_frame, text="Provider:").grid(row=0, column=0, sticky="w")
|
|
self.provider_combo = ttk.Combobox(
|
|
form_frame,
|
|
textvariable=self.provider_var,
|
|
values=self.provider_names,
|
|
state="readonly",
|
|
)
|
|
self.provider_combo.grid(row=0, column=1, sticky="ew", padx=(6, 18))
|
|
self.provider_combo.bind("<<ComboboxSelected>>", lambda event: self.fetch_models_async())
|
|
|
|
ttk.Label(form_frame, text="Model:").grid(row=0, column=2, sticky="w")
|
|
self.model_combo = ttk.Combobox(form_frame, textvariable=self.model_var, state="readonly")
|
|
self.model_combo.grid(row=0, column=3, sticky="ew", padx=(6, 18))
|
|
|
|
ttk.Label(form_frame, text="Temperature:").grid(row=0, column=4, sticky="w")
|
|
self.temperature_spin = ttk.Spinbox(
|
|
form_frame,
|
|
textvariable=self.temperature_var,
|
|
from_=0.0,
|
|
to=1.0,
|
|
increment=0.05,
|
|
width=6,
|
|
)
|
|
self.temperature_spin.grid(row=0, column=5, sticky="w")
|
|
|
|
form_frame.columnconfigure(1, weight=1)
|
|
form_frame.columnconfigure(3, weight=1)
|
|
|
|
button_frame = ttk.Frame(self)
|
|
button_frame.pack(fill="x", **padding)
|
|
|
|
self.run_button = ttk.Button(button_frame, text="Run Tests", command=self.start_run)
|
|
self.run_button.pack(side="left")
|
|
|
|
ttk.Button(button_frame, text="Refresh Models", command=self.fetch_models_async).pack(side="left", padx=(12, 0))
|
|
ttk.Button(button_frame, text="New Test", command=self.open_test_editor).pack(side="left", padx=(12, 0))
|
|
ttk.Button(button_frame, text="Download Model", command=self.open_model_downloader).pack(side="left", padx=(12, 0))
|
|
ttk.Button(button_frame, text="Edit Tests", command=lambda: open_path(TESTS_DIR)).pack(side="left", padx=(12, 0))
|
|
ttk.Button(button_frame, text="Open Results", command=lambda: open_path(RESULTS_DIR)).pack(side="left", padx=(12, 0))
|
|
ttk.Button(button_frame, text="View Latest Report", command=self.open_latest_report).pack(side="left", padx=(12, 0))
|
|
|
|
ttk.Label(self, textvariable=self.status_var).pack(anchor="w", **padding)
|
|
|
|
self.log = scrolledtext.ScrolledText(self, wrap="word", height=22)
|
|
self.log.pack(fill="both", expand=True, padx=12, pady=(0, 12))
|
|
self.log.configure(state="disabled")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app = App()
|
|
app.mainloop()
|