Skip to content

Commit 66ae094

Browse files
authored
Merge pull request #10 from GACWR/feat/viz-cli-models-3
Feat: Visualizations, Dashboards, Model Registry CLI, Project Filtering, Search, Notifications & E2E Tests
2 parents 78106db + 03872ac commit 66ae094

85 files changed

Lines changed: 11495 additions & 456 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 114 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,24 @@
3737

3838
### For Data Scientists
3939
- **Project Management** -- Organize experiments with stage-based workflow (Ideation, Development, Production)
40+
- **Project-Scoped Filtering** -- Global project selector in the topbar scopes every page (models, datasets, experiments, jobs, workspaces, features, visualizations) to a single project
4041
- **Model Editor** -- Write and edit models directly in the browser with Monaco (Python + Rust)
41-
- **Real-Time Training** -- Watch loss curves update live via SSE during training
42+
- **Model Registry & CLI** -- Search, install, and manage models from the [Open Model Registry](https://github.com/GACWR/open-model-registry) via CLI (`openmodelstudio install iris-svm`) or the in-app registry browser. Install status syncs bidirectionally between CLI and UI
43+
- **Real-Time Training** -- Watch loss curves, accuracy, and all metrics auto-update live during training with second-level duration accuracy
4244
- **Generative Output Viewer** -- See video/image/audio outputs as models train
4345
- **Experiment Tracking** -- Compare runs with parallel coordinates and sortable tables
44-
- **JupyterLab Workspaces** -- Launch cloud-native notebooks with one click
46+
- **Visualizations & Dashboards** -- 9 visualization backends (matplotlib, seaborn, plotly, bokeh, altair, plotnine, datashader, networkx, geopandas) with a unified `render()` abstraction. Combine visualizations into drag-and-drop dashboards with persistent layout
47+
- **Global Search** -- Cmd+K command palette searches across models, datasets, experiments, training jobs, projects, and visualizations with instant navigation
48+
- **Notifications** -- Real-time notification bell with unread count, grouped timeline (Today / This Week / Earlier), mark-all-read, and context-aware icons
49+
- **JupyterLab Workspaces** -- Launch cloud-native notebooks pre-loaded with tutorial notebooks (Welcome, Visualizations, Registry)
4550
- **LLM Assistant** -- Natural language control of the entire platform
4651
- **AutoML** -- Automated hyperparameter search
4752
- **Feature Store** -- Reusable features across projects
4853

4954
### For ML Engineers
5055
- **Kubernetes-Native** -- Every model trains in its own ephemeral pod
5156
- **Rust API** -- High-performance backend built with Axum + SQLx
57+
- **Python SDK & CLI** -- `pip install openmodelstudio` gives you both a Python SDK (`import openmodelstudio as oms`) and a CLI for registry management, model install/uninstall, and configuration
5258
- **GraphQL** -- Auto-generated from PostgreSQL via PostGraphile
5359
- **Streaming Data** -- Never load full datasets to disk
5460
- **One-Command Deploy** -- `make k8s-deploy` sets up everything
@@ -68,6 +74,60 @@
6874
<img src="docs/screenshots/oms-screenshot2.png" alt="OpenModelStudio Workspaces and Model Metrics" width="100%" />
6975
</p>
7076

77+
### Visualizations & Dashboards
78+
79+
Create, render, and publish data visualizations from notebooks or the in-browser editor. OpenModelStudio supports **9 visualization backends** with a unified `render()` function that auto-detects the library:
80+
81+
| Backend | Output | Use Case |
82+
|---------|--------|----------|
83+
| matplotlib | SVG | Standard plots, publication-quality figures |
84+
| seaborn | SVG | Statistical visualization, heatmaps |
85+
| plotly | JSON | Interactive charts with zoom, pan, hover |
86+
| bokeh | JSON | Interactive streaming charts |
87+
| altair | JSON | Declarative Vega-Lite specifications |
88+
| plotnine | SVG | ggplot2-style grammar of graphics |
89+
| datashader | PNG | Server-side rendering for millions of points |
90+
| networkx | SVG | Network/graph visualizations |
91+
| geopandas | SVG | Geospatial maps |
92+
93+
```python
94+
import openmodelstudio as oms
95+
96+
viz = oms.create_visualization("loss-curve", backend="plotly")
97+
output = oms.render(fig, viz_id=viz["id"]) # auto-detects backend
98+
oms.publish_visualization(viz["id"]) # available for dashboards
99+
```
100+
101+
Combine visualizations into **drag-and-drop dashboards** with resizable panels, lock/unlock layout, and persistent configuration. Each visualization also has a full **in-browser editor** (`/visualizations/{id}`) with Monaco, live preview for JSON backends, template insertion, and data/config tabs.
102+
103+
<p align="center">
104+
<img src="docs/screenshots/oms-screenshot3.png" alt="OpenModelStudio Visualization Framework" width="100%" />
105+
</p>
106+
107+
### Model Registry
108+
109+
Browse, install, and manage models from the [Open Model Registry](https://github.com/GACWR/open-model-registry) -- a public GitHub repo that acts as a decentralized model package manager.
110+
111+
**From the CLI:**
112+
```bash
113+
openmodelstudio search classification # Search by keyword
114+
openmodelstudio install iris-svm # Install a model
115+
openmodelstudio list # List installed models
116+
```
117+
118+
**From a notebook or script:**
119+
```python
120+
import openmodelstudio as oms
121+
122+
iris = oms.use_model("iris-svm") # Load from registry
123+
handle = oms.register_model("my-iris", model=iris) # Register in project
124+
job = oms.start_training(handle.model_id, wait=True) # Train it
125+
```
126+
127+
`use_model()` resolves via the platform API, so it works inside workspace containers (K8s pods) without filesystem access. If the model isn't installed yet, it auto-installs from the registry. The web UI registry page shows **Installed** / **Not Installed** badges that stay in sync with CLI operations.
128+
129+
---
130+
71131
## Quick Start
72132

73133
### Prerequisites
@@ -142,9 +202,9 @@ This will:
142202
| **Frontend** | Next.js 16, shadcn/ui, Tailwind, Recharts | App Router, Monaco editor, SSE streaming, Cmd+K search |
143203
| **API** | Rust, Axum, SQLx | JWT auth, RBAC, K8s client, SSE metrics, LLM integration |
144204
| **PostGraphile** | Node.js | Auto-generated GraphQL from PostgreSQL schema |
145-
| **PostgreSQL 16** | SQL | Primary data store: users, projects, models, jobs, datasets, experiments |
205+
| **PostgreSQL 16** | SQL | Primary data store: users, projects, models, jobs, datasets, experiments, visualizations, dashboards, notifications |
146206
| **Model Runner** | Python/Rust | Ephemeral K8s pods per training job, streaming metrics |
147-
| **JupyterHub** | Python | Per-user JupyterLab with pre-configured SDK and datasets |
207+
| **JupyterHub** | Python | Per-user JupyterLab with pre-configured SDK, tutorial notebooks, and datasets |
148208

149209
### Training Job Lifecycle
150210

@@ -161,15 +221,18 @@ User clicks "Train" --> API creates training_job record
161221
### Database Schema (Key Tables)
162222

163223
```sql
164-
users (id, email, name, password_hash, role, created_at)
165-
projects (id, name, description, stage, owner_id, created_at)
166-
models (id, project_id, name, framework, created_at)
167-
model_versions (id, model_id, version, code, created_at)
168-
jobs (id, project_id, model_id, job_type, status, config, metrics, started_at, completed_at)
169-
datasets (id, project_id, name, path, format, size_bytes, created_at)
170-
experiments (id, project_id, name, description, created_at)
171-
experiment_runs (id, experiment_id, parameters, metrics, created_at)
172-
workspaces (id, user_id, status, jupyter_url, created_at)
224+
users (id, email, name, password_hash, role, created_at)
225+
projects (id, name, description, stage, owner_id, created_at)
226+
models (id, project_id, name, framework, registry_name, created_at)
227+
model_versions (id, model_id, version, code, created_at)
228+
jobs (id, project_id, model_id, job_type, status, config, metrics, started_at, completed_at)
229+
datasets (id, project_id, name, path, format, size_bytes, created_at)
230+
experiments (id, project_id, name, description, created_at)
231+
experiment_runs (id, experiment_id, parameters, metrics, created_at)
232+
workspaces (id, user_id, status, jupyter_url, created_at)
233+
visualizations (id, project_id, name, backend, code, output_type, output_data, published, created_at)
234+
dashboards (id, project_id, name, description, layout, created_at)
235+
notifications (id, user_id, title, message, notification_type, read, link, created_at)
173236
```
174237

175238
> See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the full architecture documentation.
@@ -181,7 +244,9 @@ workspaces (id, user_id, status, jupyter_url, created_at)
181244
Follow these guides to go from zero to a fully tracked ML experiment:
182245

183246
1. **[Usage Guide](docs/USAGE.md)** -- Log in, create a project, upload a dataset, launch a workspace
184-
2. **[Modeling Guide](docs/MODELING.md)** -- Train, evaluate, and track models using the SDK (13-cell notebook walkthrough)
247+
2. **[Modeling Guide](docs/MODELING.md)** -- Train, evaluate, and track models using the SDK (16-cell notebook walkthrough including visualizations and dashboards)
248+
3. **[Visualization Guide](docs/VISUALIZATIONS.md)** -- All 9 backends, `render()` function, dashboards, and the in-browser editor (pre-loaded as `visualization.ipynb` in workspaces)
249+
4. **[Registry & CLI Guide](docs/CLI-REGISTRY.md)** -- Install, use, and manage models from the Open Model Registry (pre-loaded as `registry.ipynb` in workspaces)
185250

186251
---
187252

@@ -222,6 +287,38 @@ Follow these guides to go from zero to a fully tracked ML experiment:
222287
| `GET` | `/training/:id` | Get training job status |
223288
| `GET` | `/training/:id/metrics` | SSE stream of training metrics |
224289

290+
### Visualizations & Dashboards
291+
292+
| Method | Endpoint | Description |
293+
|--------|----------|-------------|
294+
| `GET` | `/visualizations` | List visualizations (supports `?project_id=`) |
295+
| `POST` | `/visualizations` | Create a visualization |
296+
| `GET` | `/visualizations/:id` | Get visualization details |
297+
| `PUT` | `/visualizations/:id` | Update visualization code/config |
298+
| `POST` | `/visualizations/:id/render` | Render a visualization |
299+
| `POST` | `/visualizations/:id/publish` | Publish for dashboard use |
300+
| `GET` | `/dashboards` | List dashboards |
301+
| `POST` | `/dashboards` | Create a dashboard |
302+
| `PUT` | `/dashboards/:id` | Update dashboard layout |
303+
304+
### Notifications & Search
305+
306+
| Method | Endpoint | Description |
307+
|--------|----------|-------------|
308+
| `GET` | `/notifications` | Get user notifications (supports `?unread=true`) |
309+
| `POST` | `/notifications/:id/read` | Mark notification as read |
310+
| `POST` | `/notifications/read-all` | Mark all notifications as read |
311+
| `GET` | `/search?q=` | Global search across models, datasets, experiments, jobs, projects |
312+
313+
### Model Registry
314+
315+
| Method | Endpoint | Description |
316+
|--------|----------|-------------|
317+
| `GET` | `/models/registry-status?names=` | Check install status for registry models |
318+
| `POST` | `/models/registry-install` | Register a model from the registry |
319+
| `POST` | `/models/registry-uninstall` | Unregister a registry model |
320+
| `GET` | `/sdk/models/resolve-registry/:name` | Resolve a registry model by name (used by SDK `use_model()`) |
321+
225322
### Other Endpoints
226323

227324
| Method | Endpoint | Description |
@@ -304,7 +401,9 @@ Run `make help` to see all available targets. Key ones:
304401
| Doc | Description |
305402
|-----|-------------|
306403
| [Usage Guide](docs/USAGE.md) | UI walkthrough: login, projects, datasets, workspaces |
307-
| [Modeling Guide](docs/MODELING.md) | End-to-end SDK notebook: train, evaluate, track |
404+
| [Modeling Guide](docs/MODELING.md) | End-to-end SDK notebook: train, evaluate, visualize, track |
405+
| [Visualizations Guide](docs/VISUALIZATIONS.md) | 9 backends, `render()`, dashboards, in-browser editor |
406+
| [CLI & Registry Guide](docs/CLI-REGISTRY.md) | Model registry: search, install, `use_model()`, uninstall |
308407
| [Architecture](docs/ARCHITECTURE.md) | System design, component diagram, data flow |
309408
| [Model Authoring](docs/MODEL-AUTHORING.md) | How to write models for OpenModelStudio |
310409
| [Dataset Guide](docs/DATASET-GUIDE.md) | Preparing and uploading datasets |

api/src/main.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ async fn main() {
9696
.route("/projects/{project_id}/models", get(routes::models::list))
9797
.route("/models", get(routes::models::list_all))
9898
.route("/models", post(routes::models::create))
99+
.route("/models/registry-status", get(routes::models::registry_status))
100+
.route("/models/registry-uninstall", post(routes::models::registry_uninstall))
99101
.route("/models/{id}", get(routes::models::get))
100102
.route("/models/{id}", put(routes::models::update))
101103
.route("/models/{id}", delete(routes::models::delete))
@@ -152,7 +154,9 @@ async fn main() {
152154
.route("/features/{id}", delete(routes::features::delete))
153155
// Notifications
154156
.route("/notifications", get(routes::notifications::list))
157+
.route("/notifications/unread-count", get(routes::notifications::unread_count))
155158
.route("/notifications/read", post(routes::notifications::mark_read))
159+
.route("/notifications/read-all", post(routes::notifications::mark_all_read))
156160
// Search
157161
.route("/search", get(routes::search::search))
158162
// LLM
@@ -178,6 +182,7 @@ async fn main() {
178182
.route("/sdk/datasets/{id}/upload", post(routes::sdk::dataset_upload))
179183
.route("/sdk/datasets/{id}/content", get(routes::sdk::dataset_content))
180184
.route("/sdk/models/resolve/{name_or_id}", get(routes::sdk::resolve_model))
185+
.route("/sdk/models/resolve-registry/{name}", get(routes::sdk::resolve_registry_model))
181186
.route("/sdk/models/{id}/artifact", get(routes::sdk::model_artifact))
182187
// SDK Feature Store
183188
.route("/sdk/features", post(routes::sdk::create_features))
@@ -201,6 +206,31 @@ async fn main() {
201206
.route("/sdk/sweeps", post(routes::sdk::create_sweep))
202207
.route("/sdk/sweeps/{id}", get(routes::sdk::get_sweep))
203208
.route("/sdk/sweeps/{id}/stop", post(routes::sdk::stop_sweep))
209+
// SDK Visualizations
210+
.route("/sdk/visualizations", get(routes::visualizations::list_all))
211+
.route("/sdk/visualizations", post(routes::visualizations::create))
212+
.route("/sdk/visualizations/{id}", get(routes::visualizations::get))
213+
.route("/sdk/visualizations/{id}", put(routes::visualizations::update))
214+
.route("/sdk/visualizations/{id}/publish", post(routes::visualizations::publish))
215+
.route("/sdk/visualizations/{id}/render", post(routes::visualizations::get))
216+
// SDK Dashboards
217+
.route("/sdk/dashboards", get(routes::visualizations::list_dashboards))
218+
.route("/sdk/dashboards", post(routes::visualizations::create_dashboard))
219+
.route("/sdk/dashboards/{id}", get(routes::visualizations::get_dashboard))
220+
.route("/sdk/dashboards/{id}", put(routes::visualizations::update_dashboard))
221+
// Visualizations
222+
.route("/visualizations", get(routes::visualizations::list_all))
223+
.route("/visualizations", post(routes::visualizations::create))
224+
.route("/visualizations/{id}", get(routes::visualizations::get))
225+
.route("/visualizations/{id}", put(routes::visualizations::update))
226+
.route("/visualizations/{id}", delete(routes::visualizations::delete))
227+
.route("/visualizations/{id}/publish", post(routes::visualizations::publish))
228+
// Dashboards
229+
.route("/dashboards", get(routes::visualizations::list_dashboards))
230+
.route("/dashboards", post(routes::visualizations::create_dashboard))
231+
.route("/dashboards/{id}", get(routes::visualizations::get_dashboard))
232+
.route("/dashboards/{id}", put(routes::visualizations::update_dashboard))
233+
.route("/dashboards/{id}", delete(routes::visualizations::delete_dashboard))
204234
// Admin
205235
.route("/admin/users", get(routes::admin::list_users))
206236
.route("/admin/users/{id}", put(routes::admin::update_user))

api/src/models/dataset.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use uuid::Uuid;
66
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
77
pub struct Dataset {
88
pub id: Uuid,
9-
pub project_id: Uuid,
9+
pub project_id: Option<Uuid>,
1010
pub name: String,
1111
pub description: Option<String>,
1212
pub format: String,
@@ -45,7 +45,7 @@ pub struct UploadUrlResponse {
4545
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
4646
pub struct DataSource {
4747
pub id: Uuid,
48-
pub project_id: Uuid,
48+
pub project_id: Option<Uuid>,
4949
pub name: String,
5050
pub source_type: String,
5151
pub connection_string: Option<String>,

api/src/models/model.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use uuid::Uuid;
66
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
77
pub struct Model {
88
pub id: Uuid,
9-
pub project_id: Uuid,
9+
pub project_id: Option<Uuid>,
1010
pub name: String,
1111
pub description: Option<String>,
1212
pub framework: String,
@@ -18,6 +18,7 @@ pub struct Model {
1818
pub status: String,
1919
pub language: String,
2020
pub origin_workspace_id: Option<Uuid>,
21+
pub registry_name: Option<String>,
2122
}
2223

2324
#[derive(Debug, Deserialize)]

api/src/models/pipeline.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use uuid::Uuid;
66
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
77
pub struct Pipeline {
88
pub id: Uuid,
9-
pub project_id: Uuid,
9+
pub project_id: Option<Uuid>,
1010
pub name: String,
1111
pub description: Option<String>,
1212
pub config: serde_json::Value,

api/src/routes/automl.rs

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use axum::{
2-
extract::State,
2+
extract::{Query, State},
33
Json,
44
};
55

@@ -11,26 +11,49 @@ use crate::AppState;
1111
pub async fn list_sweeps(
1212
State(state): State<AppState>,
1313
AuthUser(_claims): AuthUser,
14+
Query(params): Query<super::ProjectFilter>,
1415
) -> AppResult<Json<Vec<Experiment>>> {
15-
let sweeps: Vec<Experiment> = sqlx::query_as(
16-
"SELECT * FROM experiments WHERE experiment_type = 'automl' ORDER BY created_at DESC"
17-
)
18-
.fetch_all(&state.db)
19-
.await?;
16+
let sweeps: Vec<Experiment> = if let Some(pid) = params.project_id {
17+
sqlx::query_as(
18+
"SELECT * FROM experiments WHERE experiment_type = 'automl' AND project_id = $1 ORDER BY created_at DESC"
19+
)
20+
.bind(pid)
21+
.fetch_all(&state.db)
22+
.await?
23+
} else {
24+
sqlx::query_as(
25+
"SELECT * FROM experiments WHERE experiment_type = 'automl' ORDER BY created_at DESC"
26+
)
27+
.fetch_all(&state.db)
28+
.await?
29+
};
2030
Ok(Json(sweeps))
2131
}
2232

2333
pub async fn list_trials(
2434
State(state): State<AppState>,
2535
AuthUser(_claims): AuthUser,
36+
Query(params): Query<super::ProjectFilter>,
2637
) -> AppResult<Json<Vec<ExperimentRun>>> {
27-
let trials: Vec<ExperimentRun> = sqlx::query_as(
28-
"SELECT er.* FROM experiment_runs er
29-
JOIN experiments e ON er.experiment_id = e.id
30-
WHERE e.experiment_type = 'automl'
31-
ORDER BY er.created_at DESC"
32-
)
33-
.fetch_all(&state.db)
34-
.await?;
38+
let trials: Vec<ExperimentRun> = if let Some(pid) = params.project_id {
39+
sqlx::query_as(
40+
"SELECT er.* FROM experiment_runs er
41+
JOIN experiments e ON er.experiment_id = e.id
42+
WHERE e.experiment_type = 'automl' AND e.project_id = $1
43+
ORDER BY er.created_at DESC"
44+
)
45+
.bind(pid)
46+
.fetch_all(&state.db)
47+
.await?
48+
} else {
49+
sqlx::query_as(
50+
"SELECT er.* FROM experiment_runs er
51+
JOIN experiments e ON er.experiment_id = e.id
52+
WHERE e.experiment_type = 'automl'
53+
ORDER BY er.created_at DESC"
54+
)
55+
.fetch_all(&state.db)
56+
.await?
57+
};
3558
Ok(Json(trials))
3659
}

api/src/routes/data_sources.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use axum::{
2-
extract::{Path, State},
2+
extract::{Path, Query, State},
33
Json,
44
};
55
use uuid::Uuid;
@@ -26,12 +26,18 @@ pub async fn list(
2626
pub async fn list_all(
2727
State(state): State<AppState>,
2828
AuthUser(_claims): AuthUser,
29+
Query(params): Query<super::ProjectFilter>,
2930
) -> AppResult<Json<Vec<DataSource>>> {
30-
let sources: Vec<DataSource> = sqlx::query_as(
31-
"SELECT * FROM data_sources ORDER BY created_at DESC"
32-
)
33-
.fetch_all(&state.db)
34-
.await?;
31+
let sources: Vec<DataSource> = if let Some(pid) = params.project_id {
32+
sqlx::query_as("SELECT * FROM data_sources WHERE project_id = $1 ORDER BY created_at DESC")
33+
.bind(pid)
34+
.fetch_all(&state.db)
35+
.await?
36+
} else {
37+
sqlx::query_as("SELECT * FROM data_sources ORDER BY created_at DESC")
38+
.fetch_all(&state.db)
39+
.await?
40+
};
3541
Ok(Json(sources))
3642
}
3743

0 commit comments

Comments
 (0)