Skip to content

Commit fae12f7

Browse files
committed
Merge branch 'develop' into feature/models-tutorials
2 parents 0b19154 + 60b3111 commit fae12f7

68 files changed

Lines changed: 2766 additions & 410 deletions

Some content is hidden

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

DashAI/back/api/api_v1/api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
generative_process,
1111
generative_session,
1212
jobs,
13+
metrics,
1314
notebook,
1415
pipelines,
1516
plugins,
@@ -32,3 +33,4 @@
3233
api_router_v1.include_router(pipelines.router, prefix="/pipelines")
3334
api_router_v1.include_router(plugins.router, prefix="/plugin")
3435
api_router_v1.include_router(notebook.router, prefix="/notebook")
36+
api_router_v1.include_router(metrics.router, prefix="/metrics")

DashAI/back/api/api_v1/endpoints/experiments.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,9 @@ async def create_experiment(
215215
name=params.name,
216216
input_columns=params.input_columns,
217217
output_columns=params.output_columns,
218+
train_metrics=params.train_metrics,
219+
validation_metrics=params.validation_metrics,
220+
test_metrics=params.test_metrics,
218221
splits=params.splits,
219222
)
220223
db.add(experiment)
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import asyncio
2+
import json
3+
import logging
4+
5+
from fastapi import APIRouter, Depends, WebSocket
6+
from fastapi.websockets import WebSocketDisconnect
7+
from kink import di, inject
8+
from sqlalchemy.orm import sessionmaker
9+
10+
from DashAI.back.core.enums.status import RunStatus
11+
from DashAI.back.dependencies.database.models import Metric, Run
12+
13+
logging.basicConfig(level=logging.DEBUG)
14+
logger = logging.getLogger(__name__)
15+
16+
router = APIRouter()
17+
18+
19+
@router.websocket("/ws/{run_id}")
20+
@inject
21+
async def live_metrics_websocket(
22+
websocket: WebSocket,
23+
run_id: int,
24+
session_factory: sessionmaker = Depends(lambda: di["session_factory"]),
25+
):
26+
await websocket.accept()
27+
28+
last_timestamp = None
29+
first_send = True
30+
31+
try:
32+
while True:
33+
with session_factory() as db:
34+
query = db.query(Metric).filter(Metric.run_id == run_id)
35+
36+
# First send: get all metrics
37+
# Subsequent sends: get only new metrics
38+
if not first_send and last_timestamp is not None:
39+
query = query.filter(Metric.timestamp > last_timestamp)
40+
41+
# Order by step and timestamp to ensure correct sequence
42+
metrics = query.order_by(
43+
Metric.step,
44+
Metric.timestamp,
45+
).all()
46+
47+
run = db.get(Run, run_id)
48+
49+
# Update last_timestamp
50+
if metrics:
51+
last_timestamp = metrics[-1].timestamp
52+
53+
# Structure payload
54+
# split -> level ->
55+
# metric_name -> list of {step, value, timestamp}
56+
payload: dict[str, dict[str, dict[str, list]]] = {}
57+
for metric in metrics:
58+
split = metric.split.name
59+
level = metric.level.name
60+
name = metric.name
61+
62+
payload.setdefault(split, {}).setdefault(level, {}).setdefault(
63+
name, []
64+
).append(
65+
{
66+
"step": metric.step,
67+
"value": metric.value,
68+
"timestamp": metric.timestamp.isoformat(),
69+
}
70+
)
71+
72+
if run:
73+
payload["run_status"] = run.status.name
74+
75+
if payload:
76+
await websocket.send_text(json.dumps(payload))
77+
78+
first_send = False
79+
80+
if run and run.status in {RunStatus.FINISHED, RunStatus.ERROR}:
81+
await websocket.close(code=1000)
82+
break
83+
84+
await asyncio.sleep(1)
85+
86+
except WebSocketDisconnect:
87+
pass

DashAI/back/api/api_v1/endpoints/runs.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
from sqlalchemy.orm import sessionmaker
1212

1313
from DashAI.back.api.api_v1.schemas.runs_params import RunParams, UpdateRunParams
14+
from DashAI.back.core.enums.metrics import LevelEnum
1415
from DashAI.back.dependencies.database.models import (
1516
Experiment,
1617
GlobalExplainer,
1718
LocalExplainer,
19+
Metric,
1820
Prediction,
1921
Run,
2022
RunStatus,
@@ -26,6 +28,49 @@
2628
router = APIRouter()
2729

2830

31+
def get_metrics_for_run(db, run_id: int):
32+
"""Retrieve metrics associated with a specific run.
33+
34+
Parameters
35+
----------
36+
db : Session
37+
SQLAlchemy session to interact with the database.
38+
run_id : int
39+
ID of the run for which to retrieve metrics.
40+
41+
Returns
42+
-------
43+
dict
44+
A dictionary containing train, validation, and test metrics for the run.
45+
"""
46+
metrics = (
47+
db.query(Metric)
48+
.filter(Metric.run_id == run_id, Metric.level == LevelEnum.LAST)
49+
.all()
50+
)
51+
52+
# Initialize the response structure
53+
response = {
54+
"train_metrics": None,
55+
"validation_metrics": None,
56+
"test_metrics": None,
57+
}
58+
59+
# Group metrics by split
60+
for metric in metrics:
61+
# Determine the key in the response dictionary
62+
split_key = f"{metric.split.name.lower()}_metrics"
63+
64+
if response[split_key] is None:
65+
response[split_key] = {}
66+
67+
# In the new schema, we store 'value'.
68+
# For 'LAST' level, we just want the latest name: value pair.
69+
response[split_key][metric.name] = metric.value
70+
71+
return response
72+
73+
2974
@router.get("/")
3075
@inject
3176
async def get_runs(
@@ -67,6 +112,18 @@ async def get_runs(
67112
runs = db.scalars(
68113
select(Run).where(Run.experiment_id == experiment_id)
69114
).all()
115+
if not runs:
116+
raise HTTPException(
117+
status_code=status.HTTP_404_NOT_FOUND,
118+
detail="Runs associated with Experiment not found",
119+
)
120+
121+
# Add metrics to each run
122+
for run in runs:
123+
metrics = get_metrics_for_run(db, run.id)
124+
run.train_metrics = metrics["train_metrics"]
125+
run.validation_metrics = metrics["validation_metrics"]
126+
run.test_metrics = metrics["test_metrics"]
70127
else:
71128
runs = db.query(Run).all()
72129
except exc.SQLAlchemyError as e:
@@ -112,6 +169,12 @@ async def get_run_by_id(
112169
status_code=status.HTTP_404_NOT_FOUND,
113170
detail="Run not found",
114171
)
172+
# Add metrics to the run
173+
metrics = get_metrics_for_run(db, run_id)
174+
run.train_metrics = metrics["train_metrics"]
175+
run.validation_metrics = metrics["validation_metrics"]
176+
run.test_metrics = metrics["test_metrics"]
177+
115178
except exc.SQLAlchemyError as e:
116179
log.exception(e)
117180
raise HTTPException(
@@ -580,6 +643,11 @@ def reset_run(run):
580643
setattr(run, "delivery_time", None)
581644
setattr(run, "end_time", None)
582645

646+
# Delete metrics from DB
647+
with di["session_factory"]() as db:
648+
db.query(Metric).filter(Metric.run_id == run.id).delete()
649+
db.commit()
650+
583651
# Delete files
584652
if run.run_path and os.path.exists(run.run_path):
585653
remove_path(run.run_path)

DashAI/back/api/api_v1/schemas/experiments_params.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ class ExperimentParams(BaseModel):
99
name: str
1010
input_columns: List[str]
1111
output_columns: List[str]
12+
train_metrics: List[str]
13+
validation_metrics: List[str]
14+
test_metrics: List[str]
1215
splits: str
1316

1417

DashAI/back/core/enums/metrics.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from enum import Enum
2+
3+
4+
class SplitEnum(Enum):
5+
TRAIN = "train"
6+
VALIDATION = "validation"
7+
TEST = "test"
8+
9+
10+
class LevelEnum(Enum):
11+
LAST = "last"
12+
TRIAL = "trial"
13+
STEP = "step"
14+
EPOCH = "epoch"

DashAI/back/dependencies/database/models.py

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,20 @@
33
from datetime import datetime
44
from typing import Any, Dict, List
55

6-
from sqlalchemy import JSON, Boolean, DateTime, Enum, ForeignKey, String
6+
from sqlalchemy import (
7+
JSON,
8+
Boolean,
9+
DateTime,
10+
Enum,
11+
Float,
12+
ForeignKey,
13+
Integer,
14+
String,
15+
)
716
from sqlalchemy.ext.declarative import declarative_base
817
from sqlalchemy.orm import Mapped, mapped_column, relationship
918

19+
from DashAI.back.core.enums.metrics import LevelEnum, SplitEnum
1020
from DashAI.back.core.enums.plugin_tags import PluginTag
1121
from DashAI.back.core.enums.status import (
1222
ConverterListStatus,
@@ -92,6 +102,12 @@ class Experiment(Base):
92102
task_name: Mapped[str] = mapped_column(String, nullable=False)
93103
input_columns: Mapped[str] = mapped_column(JSON, nullable=False)
94104
output_columns: Mapped[str] = mapped_column(JSON, nullable=False)
105+
106+
# Metrics per split
107+
train_metrics: Mapped[list[str]] = mapped_column(JSON, nullable=True)
108+
validation_metrics: Mapped[list[str]] = mapped_column(JSON, nullable=True)
109+
test_metrics: Mapped[list[str]] = mapped_column(JSON, nullable=True)
110+
95111
splits: Mapped[str] = mapped_column(JSON, nullable=False)
96112
created: Mapped[DateTime] = mapped_column(DateTime, default=datetime.now)
97113
last_modified: Mapped[DateTime] = mapped_column(
@@ -134,10 +150,6 @@ class Run(Base):
134150
plot_importance_path: Mapped[str] = mapped_column(String, nullable=True)
135151
# goal metrics
136152
goal_metric: Mapped[str] = mapped_column(String)
137-
# metrics
138-
train_metrics: Mapped[JSON] = mapped_column(JSON, nullable=True)
139-
test_metrics: Mapped[JSON] = mapped_column(JSON, nullable=True)
140-
validation_metrics: Mapped[JSON] = mapped_column(JSON, nullable=True)
141153
# artifacts
142154
artifacts: Mapped[str] = mapped_column(JSON, nullable=True)
143155
# metadata
@@ -154,6 +166,7 @@ class Run(Base):
154166
predictions = relationship(
155167
"Prediction", cascade="all, delete-orphan", back_populates="run"
156168
)
169+
metrics = relationship("Metric", cascade="all, delete-orphan", back_populates="run")
157170

158171
def set_status_as_delivered(self) -> None:
159172
"""Update the status of the run to delivered and set delivery_time to now."""
@@ -223,6 +236,30 @@ def set_status_as_error(self) -> None:
223236
self.status = PredictionStatus.ERROR
224237

225238

239+
class Metric(Base):
240+
__tablename__ = "metric"
241+
"""
242+
Table to store all the information related to a metric
243+
"""
244+
id: Mapped[int] = mapped_column(primary_key=True)
245+
run_id: Mapped[int] = mapped_column(
246+
ForeignKey("run.id", ondelete="CASCADE"), index=True
247+
)
248+
split: Mapped[SplitEnum] = mapped_column(Enum(SplitEnum), nullable=False)
249+
level: Mapped[LevelEnum] = mapped_column(Enum(LevelEnum), nullable=False)
250+
251+
name: Mapped[str] = mapped_column(String, nullable=False)
252+
value: Mapped[float] = mapped_column(Float, nullable=False)
253+
step: Mapped[int] = mapped_column(Integer, nullable=False)
254+
255+
timestamp: Mapped[datetime] = mapped_column(
256+
DateTime, default=datetime.now, index=True
257+
)
258+
259+
# Relationships
260+
run: Mapped["Run"] = relationship("Run", back_populates="metrics")
261+
262+
226263
class Plugin(Base):
227264
__tablename__ = "plugin"
228265
"""

DashAI/back/initial_components.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,25 @@
7272
PipelineJob,
7373
PredictJob,
7474
)
75-
from DashAI.back.metrics import F1, MAE, RMSE, Accuracy, Bleu, Precision, Recall, Ter
75+
from DashAI.back.metrics import (
76+
F1,
77+
MAE,
78+
MSE,
79+
R2,
80+
RMSE,
81+
ROCAUC,
82+
Accuracy,
83+
Bleu,
84+
Chrf,
85+
CohenKappa,
86+
ExplainedVariance,
87+
HammingDistance,
88+
LogLoss,
89+
MedianAbsoluteError,
90+
Precision,
91+
Recall,
92+
Ter,
93+
)
7694
from DashAI.back.models import (
7795
SVC,
7896
BagOfWordsTextClassificationModel,
@@ -170,8 +188,17 @@ def get_initial_components():
170188
Recall,
171189
Bleu,
172190
Ter,
173-
MAE,
191+
Chrf,
192+
MSE,
174193
RMSE,
194+
MAE,
195+
R2,
196+
MedianAbsoluteError,
197+
ExplainedVariance,
198+
ROCAUC,
199+
LogLoss,
200+
HammingDistance,
201+
CohenKappa,
175202
# Optimizers
176203
OptunaOptimizer,
177204
HyperOptOptimizer,

0 commit comments

Comments
 (0)