Frontend: Qt6 UI Integration¶
This guide covers the GUI side of adding a new worker. Once the backend package is in place, the steps below connect it to the Qt6 interface so users can launch training runs from the GUI.
Overview¶
The frontend uses three parallel registries that all share a single
worker_id string:
graph LR
subgraph "Three Registries"
FAC["Form Factory<br/>forms/factory.py"]
CAT["Worker Catalog<br/>worker_catalog/catalog.py"]
PRES["Presenter Registry<br/>presenters/workers/registry.py"]
end
subgraph "Bridge"
HANDLER["TrainingFormHandler"]
end
subgraph "Backend"
CLIENT["TrainerClient (gRPC)"]
end
FAC -->|"create_train_form()"| HANDLER
HANDLER -->|"submit_config()"| CLIENT
CAT -.->|"capability flags"| FAC
PRES -.->|"create_tabs()"| HANDLER
style FAC fill:#4a90d9,stroke:#2e5a87,color:#fff
style CAT fill:#4a90d9,stroke:#2e5a87,color:#fff
style PRES fill:#4a90d9,stroke:#2e5a87,color:#fff
style HANDLER fill:#50c878,stroke:#2e8b57,color:#fff
style CLIENT fill:#ff7f50,stroke:#cc5500,color:#fff
Important
The worker_id string (e.g. "cleanrl_worker") must be
identical across all three registries and must match the backend
package name.
Step 1: Worker Catalog Entry¶
Add your worker to the catalog so the GUI knows what it supports.
Edit gym_gui/ui/worker_catalog/catalog.py and add a
WorkerDefinition to the tuple returned by get_worker_catalog():
WorkerDefinition(
worker_id="my_worker",
display_name="My Worker",
description="Custom RL training worker",
supports_training=True,
supports_policy_load=False,
requires_live_telemetry=True,
provides_fast_analytics=False,
supports_multi_agent=False,
)
The WorkerDefinition fields control which buttons and menu items
the GUI enables for this worker:
Field |
Effect |
|---|---|
|
Shows the “Train” button in the control panel |
|
Shows the “Load Policy” button |
|
Enables the live metrics tab during training |
|
Enables the FastLane real-time visualization tab |
|
Exposes multi-agent configuration options |
Step 2: Training Form Dialog¶
Create a QDialog subclass that collects user configuration and
returns it as a dictionary. The only contract the form must fulfill
is implementing get_config() -> dict.
Create gym_gui/ui/widgets/my_worker_train_form.py:
from PySide6 import QtWidgets
from typing import Any, Dict
class MyWorkerTrainForm(QtWidgets.QDialog):
def __init__(self, parent=None, *, default_game=None, **kwargs):
super().__init__(parent)
self.setWindowTitle("My Worker -- Training Configuration")
self._build_ui()
def _build_ui(self):
layout = QtWidgets.QFormLayout(self)
self._env_combo = QtWidgets.QComboBox()
self._env_combo.addItems(["CartPole-v1", "LunarLander-v3"])
layout.addRow("Environment:", self._env_combo)
self._steps_spin = QtWidgets.QSpinBox()
self._steps_spin.setRange(1_000, 10_000_000)
self._steps_spin.setValue(100_000)
layout.addRow("Total Steps:", self._steps_spin)
buttons = QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.Ok
| QtWidgets.QDialogButtonBox.Cancel
)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addRow(buttons)
def get_config(self) -> Dict[str, Any]:
"""Return the trainer payload.
Called by TrainingFormHandler after the dialog is accepted.
The dict must contain at minimum: ``run_name``,
``entry_point``, ``arguments``, and ``metadata``.
"""
import sys
import uuid
run_id = f"my_worker_{uuid.uuid4().hex[:8]}"
return {
"run_name": run_id,
"entry_point": sys.executable,
"arguments": ["-m", "my_worker.cli"],
"metadata": {
"ui": {
"worker_id": "my_worker",
"env_id": self._env_combo.currentText(),
},
"worker": {
"module": "my_worker.cli",
"use_grpc": True,
"config": {
"run_id": run_id,
"env_id": self._env_combo.currentText(),
"total_steps": self._steps_spin.value(),
},
},
},
}
Step 3: Self-Registration¶
Register the form with the WorkerFormFactory at module load time.
Add this block at the bottom of your form file:
# -- Self-registration (bottom of my_worker_train_form.py) --
from gym_gui.ui.forms.factory import get_worker_form_factory
_factory = get_worker_form_factory()
if not _factory.has_train_form("my_worker"):
_factory.register_train_form(
"my_worker",
lambda parent=None, **kw: MyWorkerTrainForm(parent=parent, **kw),
)
The has_train_form() guard makes the registration idempotent –
safe if the module is imported more than once.
The WorkerFormFactory supports five form buckets. Register
additional forms if your worker supports them:
Method |
When to Use |
|---|---|
|
Start a new training run |
|
Load and evaluate a trained policy |
|
Resume a previously interrupted run |
|
Run evaluation-only mode |
|
Execute a custom script |
Step 4: Worker Presenter¶
The presenter handles two responsibilities:
Build train request – convert form data into a gRPC-ready dict
Create analytics tabs – provide live QWidget tabs during training (e.g. FastLane visualization, reward plots)
Create gym_gui/ui/presenters/workers/my_worker_presenter.py:
from typing import Any, List, Optional
class MyWorkerPresenter:
@property
def id(self) -> str:
return "my_worker"
def build_train_request(
self, policy_path: Any, current_game: Optional[Any]
) -> dict:
"""Build gRPC request dict from form data."""
raise NotImplementedError(
"My Worker uses form-based submission, "
"not presenter-based."
)
def create_tabs(
self,
run_id: str,
agent_id: str,
first_payload: dict,
parent: Any,
) -> List[Any]:
"""Return worker-specific QWidget tabs for live telemetry."""
return [] # No custom tabs yet
Then register it in
gym_gui/ui/presenters/workers/__init__.py:
from .my_worker_presenter import MyWorkerPresenter
_registry.register("my_worker", MyWorkerPresenter())
Signal Flow¶
No changes are needed to TrainingFormHandler or MainWindow
wiring. The existing signal chain routes everything automatically
by worker_id:
sequenceDiagram
participant CP as ControlPanel
participant TFH as TrainingFormHandler
participant FAC as WorkerFormFactory
participant DLG as MyWorkerTrainForm
participant TC as TrainerClient (gRPC)
participant WK as Worker Process
CP->>TFH: train_agent_requested("my_worker")
TFH->>FAC: create_train_form("my_worker")
FAC-->>TFH: dialog instance
TFH->>DLG: dialog.exec()
Note over DLG: User fills form,<br/>clicks OK
DLG-->>TFH: Accepted
TFH->>DLG: dialog.get_config()
DLG-->>TFH: config dict
TFH->>TC: submit_run(config)
TC->>WK: gRPC spawn
The user selects a worker and clicks Train in the
ControlPanel.ControlPanelemitstrain_agent_requested(worker_id).TrainingFormHandler.on_train_agent_requested()looks up the form inWorkerFormFactory, opens the dialog, and waits for the user.On accept, it calls
dialog.get_config()and forwards the result toTrainerClient.submit_run()via gRPC.The Daemon spawns the worker subprocess.