mekosotto Claude Opus 4.7 (1M context) commited on
Commit
389cf2a
·
1 Parent(s): d4000ca

feat(frontend): Experiments tab — MLflow runs table + two-run diff

Browse files

- New 5th tab in main(): BBB / EEG / MRI / AI Assistant / Experiments.
- _render_experiments_tab loads /experiments/runs (cached in session
state, refresh button to invalidate), shows a runs table with run_id
prefix / experiment / start_time / status / metric+param counts.
- Two selectboxes pick run ids; 'Show diff' POSTs /experiments/diff
and renders a key/kind/A/B/differs table.
- Empty-state messaging when MLflow is disabled or no runs exist;
helpful hint to trigger a pipeline first.
- New _get() helper for symmetric GET calls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files changed (1) hide show
  1. src/frontend/app.py +94 -1
src/frontend/app.py CHANGED
@@ -204,6 +204,13 @@ def _post(endpoint: str, payload: dict) -> dict:
204
  return resp.json()
205
 
206
 
 
 
 
 
 
 
 
207
  def _render_brand_header() -> None:
208
  st.markdown(
209
  """
@@ -806,6 +813,89 @@ def _render_ai_assistant_tab() -> None:
806
  st.divider()
807
 
808
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
809
  def main() -> None:
810
  """Streamlit entrypoint. Idempotent — Streamlit re-runs on every interaction."""
811
  st.set_page_config(
@@ -827,11 +917,12 @@ def main() -> None:
827
  "Run `uvicorn src.api.main:app --port 8000` or `docker compose up`."
828
  )
829
 
830
- bbb_tab, eeg_tab, mri_tab, assistant_tab = st.tabs([
831
  "Molecule (BBB)",
832
  "Signal (EEG)",
833
  "Image (MRI)",
834
  "AI Assistant",
 
835
  ])
836
 
837
  with bbb_tab:
@@ -842,6 +933,8 @@ def main() -> None:
842
  _render_mri_tab()
843
  with assistant_tab:
844
  _render_ai_assistant_tab()
 
 
845
 
846
 
847
  if __name__ == "__main__":
 
204
  return resp.json()
205
 
206
 
207
+ def _get(path: str) -> dict:
208
+ """GET helper symmetric with _post."""
209
+ resp = httpx.get(f"{_API_URL}{path}", timeout=10.0)
210
+ resp.raise_for_status()
211
+ return resp.json()
212
+
213
+
214
  def _render_brand_header() -> None:
215
  st.markdown(
216
  """
 
813
  st.divider()
814
 
815
 
816
+ def _render_experiments_tab() -> None:
817
+ """Day-8 T2B: MLflow runs table + two-run diff (Track 5)."""
818
+ _render_section(
819
+ "Experiments — MLOps Audit",
820
+ "MLflow runs across BBB / EEG / MRI experiments",
821
+ "Lists every recorded training run; pick any two to see "
822
+ "a side-by-side metric + parameter diff. Foundation for "
823
+ "auditable, reproducible model lineage."
824
+ )
825
+
826
+ if st.button("Refresh runs", key="exp_refresh"):
827
+ st.session_state.pop("experiments_runs_cache", None)
828
+
829
+ runs = st.session_state.get("experiments_runs_cache")
830
+ if runs is None:
831
+ try:
832
+ data = _get("/experiments/runs")
833
+ runs = data.get("runs", [])
834
+ st.session_state["experiments_runs_cache"] = runs
835
+ except httpx.HTTPStatusError as e:
836
+ st.error(f"Failed to load runs (HTTP {e.response.status_code}): {e.response.text}")
837
+ return
838
+ except httpx.RequestError as e:
839
+ st.error(f"Cannot reach FastAPI at {_API_URL}: {e!r}")
840
+ return
841
+
842
+ if not runs:
843
+ st.info(
844
+ "No MLflow runs found. Trigger a pipeline (BBB / EEG / MRI) "
845
+ "first, then refresh this tab. (If MLflow is disabled via "
846
+ "NEUROBRIDGE_DISABLE_MLFLOW=1, this list will stay empty.)"
847
+ )
848
+ return
849
+
850
+ # Render the runs table with a flat preview of metrics + params
851
+ rows_preview = []
852
+ for run in runs:
853
+ rows_preview.append({
854
+ "run_id": run["run_id"][:8],
855
+ "experiment": run["experiment_name"],
856
+ "start_time": run["start_time"][:19], # YYYY-MM-DDTHH:MM:SS
857
+ "status": run["status"],
858
+ "n_metrics": len(run["metrics"]),
859
+ "n_params": len(run["params"]),
860
+ })
861
+ st.dataframe(rows_preview, use_container_width=True, hide_index=True)
862
+
863
+ # Run-vs-run diff selector
864
+ st.markdown("### Compare two runs")
865
+ run_ids = [r["run_id"] for r in runs]
866
+ if len(run_ids) < 2:
867
+ st.caption("Need at least 2 runs to compare. Trigger another pipeline.")
868
+ return
869
+
870
+ col_a, col_b = st.columns(2)
871
+ with col_a:
872
+ sel_a = st.selectbox("Run A", options=run_ids, format_func=lambda x: x[:8], key="diff_a")
873
+ with col_b:
874
+ sel_b = st.selectbox("Run B", options=run_ids, index=min(1, len(run_ids) - 1), format_func=lambda x: x[:8], key="diff_b")
875
+
876
+ if st.button("Show diff", type="primary", key="exp_diff_go"):
877
+ try:
878
+ diff = _post("/experiments/diff", {"run_id_a": sel_a, "run_id_b": sel_b})
879
+ except httpx.HTTPStatusError as e:
880
+ st.error(f"Diff failed (HTTP {e.response.status_code}): {e.response.text}")
881
+ return
882
+ rows = diff.get("rows", [])
883
+ if not rows:
884
+ st.info("Both runs have identical metrics and params (or are empty).")
885
+ return
886
+ diff_table = [
887
+ {
888
+ "key": r["key"],
889
+ "kind": r["kind"],
890
+ "A": r["value_a"] or "—",
891
+ "B": r["value_b"] or "—",
892
+ "differs": "✓" if r["differs"] else "",
893
+ }
894
+ for r in rows
895
+ ]
896
+ st.dataframe(diff_table, use_container_width=True, hide_index=True)
897
+
898
+
899
  def main() -> None:
900
  """Streamlit entrypoint. Idempotent — Streamlit re-runs on every interaction."""
901
  st.set_page_config(
 
917
  "Run `uvicorn src.api.main:app --port 8000` or `docker compose up`."
918
  )
919
 
920
+ bbb_tab, eeg_tab, mri_tab, assistant_tab, experiments_tab = st.tabs([
921
  "Molecule (BBB)",
922
  "Signal (EEG)",
923
  "Image (MRI)",
924
  "AI Assistant",
925
+ "Experiments",
926
  ])
927
 
928
  with bbb_tab:
 
933
  _render_mri_tab()
934
  with assistant_tab:
935
  _render_ai_assistant_tab()
936
+ with experiments_tab:
937
+ _render_experiments_tab()
938
 
939
 
940
  if __name__ == "__main__":