refactor(dashboard): switch a NiceGUI, rimuovi legacy Streamlit

NiceGUI è la dashboard ufficiale (port 8080, dark/neon palette, 3 pagine:
/, /convergence, /genomes). La porta è ora parametrica via env
SWARM_DASHBOARD_PORT, letta in ui.run() — Docker la usa anche per
healthcheck e label Traefik.

docker-compose.yml: entrypoint del servizio dashboard cambiato da
streamlit a python -m multi_swarm.dashboard.nicegui_app. Default porta
8080 ovunque (.env, .env.example, compose, healthcheck).

Rimossi i file legacy della vecchia GUI Streamlit:
- src/multi_swarm/dashboard/streamlit_app.py
- src/multi_swarm/dashboard/aquarium.py (helper canvas HTML5)
- src/multi_swarm/dashboard/pages/{01_overview,02_ga_convergence,
  03_genomes,04_aquarium}.py
- tests/integration/test_streamlit_smoke.py

pyproject.toml: rimossa la dep streamlit; uv.lock rigenerato (10 deps
transitive eliminate: pydeck, watchdog, jsonschema, pillow, ecc.).
README aggiornato (architettura, comando dashboard, sezione Dashboard
ora descrive NiceGUI con riferimento al deploy Docker via Traefik).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-05-14 12:15:11 +00:00
parent 8e5efde219
commit 03f723f7fc
13 changed files with 24 additions and 1569 deletions
-310
View File
@@ -1,310 +0,0 @@
import importlib
import pandas as pd
def test_streamlit_app_imports():
importlib.import_module("multi_swarm.dashboard.data")
def test_dashboard_data_helpers_signatures():
from multi_swarm.dashboard import data
assert hasattr(data, "list_runs_df")
assert hasattr(data, "generations_df")
assert hasattr(data, "evaluations_df")
assert hasattr(data, "genomes_df")
def test_aquarium_helper_builds_html_with_click_handler():
from multi_swarm.dashboard.aquarium import build_aquarium_html
fish = [
{
"id": "abc123",
"fitness": 0.8,
"cognitive_style": "physicist",
"n_trades": 30,
"dsr": 0.7,
"sharpe": 1.2,
"max_dd": 0.1,
"system_prompt": "test",
"temperature": 0.9,
"lookback_window": 200,
"feature_access": ["close"],
"model_tier": "C",
"generation": 1,
"parent_ids": [],
"ancestors": [],
}
]
html = build_aquarium_html(fish, canvas_w=800, canvas_h=400)
assert "canvas" in html
assert "abc123" in html # fish id present in JSON payload
assert "addEventListener('click'" in html
assert "fish-info-panel" in html
assert "showFishInfo" in html
assert "Discendenza" in html
assert "requestAnimationFrame" in html
def test_aquarium_build_fish_dataset_legacy_path():
from multi_swarm.dashboard.aquarium import build_fish_dataset
df = pd.DataFrame(
[
{
"genome_id": "low",
"fitness": 0.1,
"cognitive_style": "physicist",
"n_trades": 1,
"dsr": 0.0,
},
{
"genome_id": "high",
"fitness": 0.9,
"cognitive_style": "biologist",
"n_trades": 10,
"dsr": 0.5,
},
]
)
out = build_fish_dataset(df)
ids = {f["id"] for f in out}
assert ids == {"low", "high"}
high = next(f for f in out if f["id"] == "high")
assert high["cognitive_style"] == "biologist"
assert high["ancestors"] == []
def test_aquarium_build_fish_dataset_drops_nan_fitness():
from multi_swarm.dashboard.aquarium import build_fish_dataset
df = pd.DataFrame(
[
{
"genome_id": "ok",
"fitness": 0.4,
"cognitive_style": "historian",
"n_trades": 2,
"dsr": 0.1,
},
{
"genome_id": "bad",
"fitness": float("nan"),
"cognitive_style": "ecologist",
"n_trades": 0,
"dsr": 0.0,
},
]
)
out = build_fish_dataset(df)
assert len(out) == 1
assert out[0]["id"] == "ok"
def test_aquarium_empty_input_returns_empty():
from multi_swarm.dashboard.aquarium import build_aquarium_html, build_fish_dataset
assert build_fish_dataset(pd.DataFrame()) == []
html = build_aquarium_html([], canvas_w=400, canvas_h=200)
assert "canvas" in html
assert "Acquario vuoto" in html
def test_build_lineage_index_returns_dict_keyed_by_id():
from multi_swarm.dashboard.aquarium import build_lineage_index
genomes = pd.DataFrame(
[
{
"id": "g1",
"generation_idx": 0,
"generation": 0,
"system_prompt": "x",
"feature_access": ["close"],
"temperature": 0.9,
"top_p": 0.95,
"model_tier": "C",
"lookback_window": 100,
"cognitive_style": "physicist",
"parent_ids": [],
},
{
"id": "g2",
"generation_idx": 1,
"generation": 1,
"system_prompt": "y",
"feature_access": ["close", "volume"],
"temperature": 1.0,
"top_p": 0.95,
"model_tier": "C",
"lookback_window": 200,
"cognitive_style": "biologist",
"parent_ids": ["g1"],
},
]
)
evals = pd.DataFrame(
[
{
"genome_id": "g1",
"fitness": 0.5,
"dsr": 0.6,
"sharpe": 1.2,
"max_dd": 0.1,
"n_trades": 30,
"parse_error": None,
"raw_text": "",
},
{
"genome_id": "g2",
"fitness": 0.7,
"dsr": 0.8,
"sharpe": 1.5,
"max_dd": 0.05,
"n_trades": 40,
"parse_error": None,
"raw_text": "",
},
]
)
idx = build_lineage_index(genomes, evals)
assert "g1" in idx and "g2" in idx
assert idx["g2"]["parent_ids"] == ["g1"]
assert idx["g2"]["fitness"] == 0.7
assert idx["g1"]["cognitive_style"] == "physicist"
assert idx["g2"]["feature_access"] == ["close", "volume"]
def test_trace_ancestors_walks_levels():
from multi_swarm.dashboard.aquarium import trace_ancestors
idx = {
"child": {
"id": "child",
"parent_ids": ["p1", "p2"],
"fitness": 0.8,
"generation": 2,
"cognitive_style": "physicist",
},
"p1": {
"id": "p1",
"parent_ids": ["gp1"],
"fitness": 0.5,
"generation": 1,
"cognitive_style": "biologist",
},
"p2": {
"id": "p2",
"parent_ids": [],
"fitness": 0.3,
"generation": 1,
"cognitive_style": "engineer",
},
"gp1": {
"id": "gp1",
"parent_ids": [],
"fitness": 0.2,
"generation": 0,
"cognitive_style": "historian",
},
}
levels = trace_ancestors("child", idx, max_levels=5)
assert len(levels) == 2
assert {a["id"] for a in levels[0]} == {"p1", "p2"}
assert {a["id"] for a in levels[1]} == {"gp1"}
def test_trace_ancestors_handles_cycles():
from multi_swarm.dashboard.aquarium import trace_ancestors
# Pathological cycle: a <-> b. Should terminate cleanly.
idx = {
"a": {
"id": "a",
"parent_ids": ["b"],
"fitness": 0.1,
"generation": 1,
"cognitive_style": "physicist",
},
"b": {
"id": "b",
"parent_ids": ["a"],
"fitness": 0.2,
"generation": 0,
"cognitive_style": "biologist",
},
}
levels = trace_ancestors("a", idx, max_levels=5)
# a -> b at level 0; b's only parent is a, already seen -> stop.
assert len(levels) == 1
assert levels[0][0]["id"] == "b"
def test_trace_ancestors_no_parents_returns_empty():
from multi_swarm.dashboard.aquarium import trace_ancestors
idx = {
"solo": {
"id": "solo",
"parent_ids": [],
"fitness": 0.4,
"generation": 0,
"cognitive_style": "engineer",
},
}
assert trace_ancestors("solo", idx) == []
def test_build_fish_dataset_attaches_ancestors():
from multi_swarm.dashboard.aquarium import build_fish_dataset, build_lineage_index
genomes = pd.DataFrame(
[
{
"id": "p",
"generation_idx": 0,
"generation": 0,
"system_prompt": "p",
"feature_access": ["close"],
"temperature": 0.8,
"top_p": 0.9,
"model_tier": "C",
"lookback_window": 100,
"cognitive_style": "physicist",
"parent_ids": [],
},
{
"id": "c",
"generation_idx": 1,
"generation": 1,
"system_prompt": "c",
"feature_access": ["close"],
"temperature": 0.8,
"top_p": 0.9,
"model_tier": "C",
"lookback_window": 120,
"cognitive_style": "biologist",
"parent_ids": ["p"],
},
]
)
evals = pd.DataFrame(
[
{"genome_id": "p", "fitness": 0.3, "dsr": 0.0, "sharpe": 0.0,
"max_dd": 0.0, "n_trades": 0},
{"genome_id": "c", "fitness": 0.6, "dsr": 0.0, "sharpe": 0.0,
"max_dd": 0.0, "n_trades": 0},
]
)
lineage = build_lineage_index(genomes, evals)
active = genomes[genomes["generation_idx"] == 1].merge(
evals, left_on="id", right_on="genome_id", how="left"
)
fish = build_fish_dataset(active, lineage)
assert len(fish) == 1
assert fish[0]["id"] == "c"
assert len(fish[0]["ancestors"]) == 1
assert fish[0]["ancestors"][0][0]["id"] == "p"