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 (
|
from multi_swarm.dashboard.data import (
|
||||||
evaluations_df,
|
evaluations_df,
|
||||||
generations_df,
|
generations_df,
|
||||||
|
genomes_df,
|
||||||
get_repo,
|
get_repo,
|
||||||
get_run_overview,
|
get_run_overview,
|
||||||
list_runs_df,
|
list_runs_df,
|
||||||
@@ -110,7 +111,11 @@ def _build_header(active: str) -> None:
|
|||||||
ui.label("⚫🔵 Multi-Swarm Coevolutivo").classes("text-h6").style(
|
ui.label("⚫🔵 Multi-Swarm Coevolutivo").classes("text-h6").style(
|
||||||
f"color: {COLOR_TEXT}; font-weight: 700;"
|
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"
|
cls = "nav-link active" if active == path else "nav-link"
|
||||||
ui.link(label, path).classes(cls)
|
ui.link(label, path).classes(cls)
|
||||||
ui.label(f"DB: {Path(DB_PATH).resolve().name}").classes("text-caption").style(
|
ui.label(f"DB: {Path(DB_PATH).resolve().name}").classes("text-caption").style(
|
||||||
@@ -416,6 +421,178 @@ def convergence() -> None:
|
|||||||
refresh()
|
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:
|
def main() -> None:
|
||||||
app.on_startup(lambda: print(f"DB: {Path(DB_PATH).resolve()}"))
|
app.on_startup(lambda: print(f"DB: {Path(DB_PATH).resolve()}"))
|
||||||
ui.run(
|
ui.run(
|
||||||
|
|||||||
Reference in New Issue
Block a user