feat(dashboard): NiceGUI Genomes page con top-K + ispezione singolo genoma
Aggiunta route /genomes: - Run selector + Top K (10/25/50) - Tabella sortable con fitness, DSR, Sharpe, max DD, trades, style, T, lookback - Selezione riga → pannello ispezione con 6 metric card e blocchi code per system_prompt + raw_text + parse_error eventuale - Auto-refresh ogni 3s (mantiene selezione corrente se ancora valida) - Header nav aggiornato con link Genomes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,7 @@ from nicegui import app, ui
|
||||
from multi_swarm.dashboard.data import (
|
||||
evaluations_df,
|
||||
generations_df,
|
||||
genomes_df,
|
||||
get_repo,
|
||||
get_run_overview,
|
||||
list_runs_df,
|
||||
@@ -110,7 +111,11 @@ def _build_header(active: str) -> None:
|
||||
ui.label("⚫🔵 Multi-Swarm Coevolutivo").classes("text-h6").style(
|
||||
f"color: {COLOR_TEXT}; font-weight: 700;"
|
||||
)
|
||||
for path, label in (("/", "Overview"), ("/convergence", "GA Convergence")):
|
||||
for path, label in (
|
||||
("/", "Overview"),
|
||||
("/convergence", "GA Convergence"),
|
||||
("/genomes", "Genomes"),
|
||||
):
|
||||
cls = "nav-link active" if active == path else "nav-link"
|
||||
ui.link(label, path).classes(cls)
|
||||
ui.label(f"DB: {Path(DB_PATH).resolve().name}").classes("text-caption").style(
|
||||
@@ -416,6 +421,178 @@ def convergence() -> None:
|
||||
refresh()
|
||||
|
||||
|
||||
@ui.page("/genomes")
|
||||
def genomes() -> None:
|
||||
_apply_theme()
|
||||
_build_header(active="/genomes")
|
||||
|
||||
options = _runs_options()
|
||||
if not options:
|
||||
ui.label("Nessuna run nel database.").classes("text-h5")
|
||||
return
|
||||
|
||||
state: dict[str, Any] = {
|
||||
"run_id": next(iter(options)),
|
||||
"selected_gid": None,
|
||||
"merged": None,
|
||||
}
|
||||
|
||||
with ui.row().classes("w-full items-center gap-4 q-mb-md"):
|
||||
select = ui.select(options=options, value=state["run_id"], label="Run").classes(
|
||||
"flex-grow"
|
||||
)
|
||||
top_k_select = ui.select(
|
||||
options={10: "Top 10", 25: "Top 25", 50: "Top 50"},
|
||||
value=10,
|
||||
label="Top K",
|
||||
)
|
||||
ui.button("🔄 Refresh", on_click=lambda: refresh()).props("outline color=primary")
|
||||
|
||||
ui.label("Top genomi per fitness").classes("text-subtitle1 q-mt-sm")
|
||||
top_table = ui.table(
|
||||
columns=[
|
||||
{"name": "genome_id", "label": "id", "field": "genome_id", "align": "left"},
|
||||
{"name": "fitness", "label": "fitness", "field": "fitness", "sortable": True},
|
||||
{"name": "dsr", "label": "DSR", "field": "dsr"},
|
||||
{"name": "sharpe", "label": "Sharpe", "field": "sharpe"},
|
||||
{"name": "max_dd", "label": "max DD", "field": "max_dd"},
|
||||
{"name": "n_trades", "label": "trades", "field": "n_trades"},
|
||||
{"name": "cognitive_style", "label": "style", "field": "cognitive_style"},
|
||||
{"name": "temperature", "label": "T", "field": "temperature"},
|
||||
{"name": "lookback_window", "label": "lookback", "field": "lookback_window"},
|
||||
],
|
||||
rows=[],
|
||||
row_key="genome_id",
|
||||
selection="single",
|
||||
).classes("w-full")
|
||||
|
||||
ui.separator().classes("q-my-md")
|
||||
|
||||
with ui.card().classes("w-full"):
|
||||
ui.label("Ispezione genoma").classes("text-subtitle1")
|
||||
detail_hint = ui.label("Seleziona un genoma dalla tabella sopra.").classes(
|
||||
"text-caption"
|
||||
).style(f"color: {COLOR_TEXT_MUTED};")
|
||||
|
||||
with ui.row().classes("w-full gap-4 q-mt-sm"):
|
||||
with ui.card().classes("flex-grow metric-card"):
|
||||
ui.label("fitness").classes("text-caption")
|
||||
fit_lbl = ui.label("—").classes("text-h4")
|
||||
with ui.card().classes("flex-grow metric-card"):
|
||||
ui.label("DSR").classes("text-caption")
|
||||
dsr_lbl = ui.label("—").classes("text-h4")
|
||||
with ui.card().classes("flex-grow metric-card"):
|
||||
ui.label("Sharpe").classes("text-caption")
|
||||
sharpe_lbl = ui.label("—").classes("text-h4")
|
||||
with ui.card().classes("flex-grow metric-card"):
|
||||
ui.label("max DD").classes("text-caption")
|
||||
dd_lbl = ui.label("—").classes("text-h4")
|
||||
with ui.card().classes("flex-grow metric-card"):
|
||||
ui.label("trades").classes("text-caption")
|
||||
trades_lbl = ui.label("—").classes("text-h4")
|
||||
with ui.card().classes("flex-grow metric-card"):
|
||||
ui.label("style").classes("text-caption")
|
||||
style_lbl = ui.label("—").classes("text-h4")
|
||||
|
||||
ui.label("System prompt").classes("text-subtitle1 q-mt-md")
|
||||
prompt_code = ui.code("—", language="text").classes("w-full")
|
||||
|
||||
ui.label("Raw LLM output").classes("text-subtitle1 q-mt-md")
|
||||
raw_code = ui.code("—", language="text").classes("w-full")
|
||||
|
||||
parse_error_lbl = ui.label("").classes("q-mt-sm").style(
|
||||
"color: #FF6B6B; font-weight: 600;"
|
||||
)
|
||||
|
||||
def _render_detail(row: dict[str, Any]) -> None:
|
||||
detail_hint.text = f"Genoma: {row.get('genome_id', '—')}"
|
||||
fit_lbl.text = f"{float(row.get('fitness', 0)):.4f}"
|
||||
dsr_lbl.text = f"{float(row.get('dsr', 0)):.4f}"
|
||||
sharpe_lbl.text = f"{float(row.get('sharpe', 0)):.3f}"
|
||||
dd_lbl.text = f"{float(row.get('max_dd', 0)):.3f}"
|
||||
trades_lbl.text = str(int(row.get("n_trades", 0)))
|
||||
style_lbl.text = str(row.get("cognitive_style", "—"))
|
||||
prompt_code.content = str(row.get("system_prompt", "—"))
|
||||
raw_code.content = str(row.get("raw_text", "—") or "—")
|
||||
pe = row.get("parse_error")
|
||||
parse_error_lbl.text = f"❌ Parse error: {pe}" if pe else ""
|
||||
|
||||
def refresh() -> None:
|
||||
run_id = select.value
|
||||
if not run_id:
|
||||
return
|
||||
try:
|
||||
repo = get_repo(DB_PATH)
|
||||
evals = evaluations_df(repo, run_id)
|
||||
gens = genomes_df(repo, run_id)
|
||||
except Exception as e: # noqa: BLE001
|
||||
ui.notify(f"Errore: {e}", type="negative")
|
||||
return
|
||||
|
||||
if evals.empty:
|
||||
top_table.rows = []
|
||||
top_table.update()
|
||||
return
|
||||
|
||||
merged = evals.merge(
|
||||
gens, left_on="genome_id", right_on="id", how="left", suffixes=("", "_g")
|
||||
)
|
||||
state["merged"] = merged
|
||||
|
||||
k = int(top_k_select.value)
|
||||
top = merged.sort_values("fitness", ascending=False).head(k)
|
||||
|
||||
rows = []
|
||||
for _, r in top.iterrows():
|
||||
rows.append(
|
||||
{
|
||||
"genome_id": str(r.get("genome_id", "—"))[:12] + "…",
|
||||
"fitness": round(float(r.get("fitness", 0)), 4),
|
||||
"dsr": round(float(r.get("dsr", 0)), 4),
|
||||
"sharpe": round(float(r.get("sharpe", 0)), 3),
|
||||
"max_dd": round(float(r.get("max_dd", 0)), 3),
|
||||
"n_trades": int(r.get("n_trades", 0)),
|
||||
"cognitive_style": str(r.get("cognitive_style", "—")),
|
||||
"temperature": round(float(r.get("temperature", 0)), 2),
|
||||
"lookback_window": int(r.get("lookback_window", 0)),
|
||||
"_full_id": str(r.get("genome_id", "")),
|
||||
}
|
||||
)
|
||||
top_table.rows = rows
|
||||
top_table.update()
|
||||
|
||||
sel = state.get("selected_gid")
|
||||
if sel:
|
||||
match = merged[merged["genome_id"] == sel]
|
||||
if not match.empty:
|
||||
_render_detail(match.iloc[0].to_dict())
|
||||
|
||||
def on_row_selected(e: Any) -> None:
|
||||
if not e.selection:
|
||||
return
|
||||
full_id = e.selection[0].get("_full_id")
|
||||
if not full_id:
|
||||
return
|
||||
state["selected_gid"] = full_id
|
||||
merged = state.get("merged")
|
||||
if merged is None:
|
||||
return
|
||||
match = merged[merged["genome_id"] == full_id]
|
||||
if not match.empty:
|
||||
_render_detail(match.iloc[0].to_dict())
|
||||
|
||||
def on_select_change() -> None:
|
||||
state["run_id"] = select.value
|
||||
state["selected_gid"] = None
|
||||
refresh()
|
||||
|
||||
select.on_value_change(on_select_change)
|
||||
top_k_select.on_value_change(lambda _: refresh())
|
||||
top_table.on("selection", on_row_selected)
|
||||
ui.timer(REFRESH_INTERVAL_S, refresh)
|
||||
refresh()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
app.on_startup(lambda: print(f"DB: {Path(DB_PATH).resolve()}"))
|
||||
ui.run(
|
||||
|
||||
Reference in New Issue
Block a user